• Static Analysis Tools for Android
  • Cristopher Oyarzun
  • Translation from: The Gold Project
  • This article is permalink: github.com/xitu/gold-m…
  • Translator: Kimhooo
  • Proofreader: PassionPenguin, PingHGao

Android static analysis tool

Let’s take a look at some of the most popular static code analysis tools that allow you to implement and enforce custom rules in your code base. There are many benefits to using Lint, including programmatically executing specifications and automating code quality and code maintenance.

In Android Studio, you may be familiar with these messages:

You can write your own rules using the following tools:

  • Android Lint API
  • ktlint
  • detekt

We will describe, step by step, the process of writing some rules on the demo project, which you can find here.

Custom rules that use the Android Lint API

First, we’ll write rules using the Android Lint API. The advantages of this include:

  • You can write rules for Java, Kotlin, Gradle, XML, and a few other file types.
  • You don’t need to add plug-ins to see warnings or errors in Android Studio.
  • Easier integration into projects.

One of the drawbacks is this footnote in their GitHub repository:

Lint API is not a final version of API; If you rely on it, be prepared to tweak your code for the next version of the tool.

So, here are the steps to create the first rule:

  1. Create a new module in the project where the custom rule resides. We will name this module asandroid-lint-rules.
  2. Modify the build.gradle file on the module as follows:
apply plugin: 'kotlin'
apply plugin: 'com.android.lint'


dependencies {
    compileOnly "com.android.tools.lint:lint-api:$lintVersion"

    testImplementation "com.android.tools.lint:lint:$lintVersion"
    testImplementation "com.android.tools.lint:lint-tests:$lintVersion"
}

jar {
    manifest {
        attributes("Lint-Registry-v2": "dev.cristopher.lint.DefaultIssueRegistry")}}Copy the code

Here, we are in the form of compileOnly import dependency, it will allow us to write custom rules. Com. Android tools. Lint: lint – API. You also need to note that I’m using lint-API: version 27.2.0 (a beta version).

Here we also specify Lint-registry-v2 to point to the class that contains the rule list.

  1. Write the first rule to avoid hard-coded colors in our layout files.
@Suppress("UnstableApiUsage")
class HardcodedColorXmlDetector : ResourceXmlDetector() {

    companion object {
        val REGEX_HEX_COLOR = "#] [a - fA - F \ \ d {3, 8}".toRegex()

        val ISSUE = Issue.create(
            id = "HardcodedColorXml",
            briefDescription = "Disallow hard-coded colors in XML layout files.",
            explanation = "Hard-coded colors should be declared as '
      
       ' resources"
      ,
            category = Category.CORRECTNESS,
            severity = Severity.ERROR,
            implementation = Implementation(
                HardcodedColorXmlDetector::class.java,
                Scope.RESOURCE_FILE_SCOPE
            )
        )
    }

    override fun getApplicableAttributes(a): Collection<String>? {
        // This method returns a set of attribute names to parse.
        // Whenever the Lint tool sees one of these properties in an XML resource file
        // the following 'visitAttribute' method is called.
        // In this case, we want to analyze every attribute in every XML resource file.
        return XmlScannerConstants.ALL
    }

    override fun visitAttribute(context: XmlContext, attribute: Attr) {
        // Get the value of the XML attribute.
        val attributeValue = attribute.nodeValue
        if (attributeValue.matches(REGEX_HEX_COLOR)) {
            context.report(
                issue = ISSUE,
                scope = attribute,
                location = context.getValueLocation(attribute),
                message = "The hexadecimal value of the hard-coded color should be declared in the '
      
       ' resource"
      )}}}Copy the code

Depending on the rules we are implementing, we will extend the different Detector classes. An Detector class can detect specific problems. Each Issue type is uniquely identified as an Issue. In this case, we will use ResourceXmlDetector because we are checking for the hexadecimal value of the hard-coded color in each XML resource.

After the class declaration, we create all the information we need to define the Issue. Here, we can specify the category and severity, as well as the interpretation that will be displayed in the integrated development environment (IDE) when the rule is triggered.

Then we need to specify the properties to scan. We can return a specific property list such as mutableListOf(“textColor”, “Background “) or return XmlScannerConstants.ALL to scan ALL properties on each layout. This will depend on your use case.

Finally, we must add the logic needed to determine whether the attribute is a hexadecimal color in order to generate a report.

  1. Create a name calledDefaultIssuereRegistryThe expanded theIssuereRegistryIn the class. And then I have to rewriteissuesVariables and list all of these variables.

If you want to create more rules, you need to add all rules here.

class DefaultIssueRegistry : IssueRegistry() {
    override val issues = listOf(
        HardcodedHexColorXmlDetector.ISSUE
    )

    override val api: Int
        get() = CURRENT_API
}

Copy the code
  1. To check that the rules are doing their job correctly, we will implement some tests. We need to build on gradle this two dependencies as testImplementation: com. Android. Tools. Lint: lint – tests and com. Android. Tools. Lint: lint. This will allow us to define an XML file in the code and scan its contents to see if the rules are working properly.

  2. If a custom attribute is used, the first test checks that the rule is still valid. So TextView will contain a property called someCustomColor with the color # FFF. We can then add a few questions to scan the mock file, and in our case, we only specify the rule we only wrote. Finally, we say that the expected outcome should be a problem of the wrong severity.

  3. In the second test, the behavior was very similar. The only change is that we are testing our rule with a common property, hexadecimal color including alpha transparency.

  4. In the previous test, if we used our resource to specify the color, we checked to see if the rule did not raise any errors. In this case, we use @color/primaryColor to set the text color, with the expected result being a clean execution.

class HardcodedColorXmlDetectorTest {

    @Test
    fun `Given a hardcoded color on a custom text view property, When we analyze our custom rule, Then display an error`(a) {
        lint()
            .files(
                xml(
                    "res/layout/layout.xml".""" 
       """
                ).indented()
            )
            .issues(HardcodedColorXmlDetector.ISSUE)
            .allowMissingSdk()
            .run()
            .expectCount(1, Severity.ERROR)
    }

    @Test
    fun `Given a hardcoded color on a text view, When we analyze our custom rule, Then display an error`(a) {
        lint()
            .files(
                xml(
                    "res/layout/layout.xml".""" 
       """
                ).indented()
            )
            .issues(HardcodedColorXmlDetector.ISSUE)
            .allowMissingSdk()
            .run()
            .expectCount(1, Severity.ERROR)
    }

    @Test
    fun `Given a color from our resources on a text view, When we analyze our custom rule, Then expect no errors`(a) {
        lint()
            .files(
                xml(
                    "res/layout/layout.xml".""" 
       """
                ).indented()
            )
            .issues(HardcodedColorXmlDetector.ISSUE)
            .allowMissingSdk()
            .run()
            .expectClean()
    }
}

Copy the code
  1. Now in the App Module, we’re going to apply all of these rules. We’re going to add this line to the build.gradle file:
dependencies {
       lintChecks project(':android-lint-rules')
    ....
}
Copy the code

In this way! If we try to set a hard-coded color in any layout, an error 🎉 is immediately displayed

If you need more ideas to add some custom rules, this repository is a great learning material: github.com/vanniktech/…

Use ktLint to customize rules

Ktlint defines itself as an anti-verbose Kotlin Lint tool with built-in formatting. One of the coolest things is that you can write your rules as well as a way to auto-correct problems, so users can easily solve problems. One drawback is that it’s written specifically for the Kotlin language, so you can’t write rules for XML resource files like we did earlier. Also, if you want to visualize problems on Android Studio, you’ll need to install a plugin. I am using this plugin: plugins.jetbrains.com/plugin/1505…

So, in this case, we’re going to enforce a rule about the Clean architecture. You may have heard that we should not expose models from the data layer of the domain or presentation layer. Some people add prefixes to each model in the data layer to facilitate identification. In this case, we are checking that each model of a package ending in data.dto should have a prefix data in its name.

Here are the steps to write a rule using KtLint:

  1. Create a new module for the custom rule. We will call this modulektlint-rules.
  2. Modify the build.gradle file on this module:
plugins {
    id 'kotlin'
}

dependencies {
    compileOnly "com.github.shyiko.ktlint:ktlint-core:$ktlintVersion"

    testImplementation "junit:junit:$junitVersion"
    testImplementation "org.assertj:assertj-core:$assertjVersion"
    testImplementation "com.github.shyiko.ktlint:ktlint-core:$ktlintVersion"
    testImplementation "com.github.shyiko.ktlint:ktlint-test:$ktlintVersion"
}
Copy the code
  1. Write a rule that is mandatory in order todata.dtoUse the prefix (Data).

First, we need to extend the Rule class ktLint provides for us and specify an ID for your Rule.

And then we’ll rewrite the visit function. Here we will set conditions to check if the package ends with data.dto and to verify that the classes in the file have the prefix data. If the class does not have this prefix, then we will use emit lambda to trigger the report, and we will also provide a way to resolve the problem.

class PrefixDataOnDtoModelsRule : Rule("prefix-data-on-dto-model") {

    companion object {
        const val DATA_PREFIX = "Data"
        const val IMPORT_DTO = "data.dto"
    }

    override fun visit(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int.errorMessage: String.canBeAutoCorrected: Boolean) - >Unit
    ) {
        if (node.elementType == ElementType.PACKAGE_DIRECTIVE) {
            val qualifiedName = (node.psi as KtPackageDirective).qualifiedName
            if (qualifiedName.isEmpty()) {
                return
            }

            if (qualifiedName.endsWith(IMPORT_DTO)) {
                node.treeParent.children().forEach {
                    checkClassesWithoutDataPrefix(it, autoCorrect, emit)
                }
            }
        }
    }

    private fun checkClassesWithoutDataPrefix(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int.errorMessage: String.canBeAutoCorrected: Boolean) - >Unit
    ) {
        if (node.elementType == ElementType.CLASS) {
            val klass = node.psi as KtClass
            if (klass.name?.startsWith(DATA_PREFIX, ignoreCase = true) != true) {
                emit(
                    node.startOffset,
                    "'${klass.name}' class is not using " +
                        "the prefix Data. Classes inside any 'data.dto' package should " +
                        "use that prefix".true
                )
                if (autoCorrect) {
                    klass.setName("$DATA_PREFIX${klass.name}")}}}}}Copy the code
  1. Create a name calledCustomRuleshiyongSetProviderClass, which extendsRuleSetProviderAnd then I have to rewrite itget()Function and lists all rules in it.
class CustomRuleSetProvider : RuleSetProvider {
    private val ruleSetId: String = "custom-ktlint-rules"

    override fun get(a) = RuleSet(ruleSetId, PrefixDataOnDtoModelsRule())
}

Copy the code
  1. Create a file in the resources/ meta-INF /services folder. This file must contain the path of the class created in Step 4.

  1. Now in our project, we will add this module so that we can apply the rules. We also created a task to execute ktLint and generate a report:
configurations { ktlint } dependencies { ktlint "com.github.shyiko:ktlint:$ktlintVersion" ktlint project(":ktlint-rules") ... } task ktlint(type: JavaExec, group: "verification", description: "Runs ktlint.") { def outputDir = "${project.buildDir}/reports/ktlint/" main = "com.github.shyiko.ktlint.Main" classpath  = configurations.ktlint args = [ "--reporter=plain", "--reporter=checkstyle,output=${outputDir}ktlint-checkstyle-report.xml", "src/**/*.kt" ] inputs.files( fileTree(dir: "src", include: "**/*.kt"), fileTree(dir: ".", include: "**/.editorconfig") ) outputs.dir(outputDir) }Copy the code
  1. I also strongly recommend that you install this plug-in so that you can be notified of any errors in the same Android Studio project.

To view your custom rules in Android Studio, you need to generate a JAR from the module and add the path to the external Rulset JARs, as follows:

Custom rules using Detekt

Detekt is a static code analysis tool for the Kotlin programming language. It operates on an abstract syntax tree provided by the Kotlin compiler. They focus on finding code odors, although you can also use them as formatting tools.

If you want to visualize these issues on Android Studio, you’ll need to install a plugin. I am using this: plugins.jetbrains.com/plugin/1076…

The rules we will implement will force a specific prefix for the repository implementation. This is just to illustrate that we can create custom standards in our projects. In this case, if we have a ProductRepository interface, we want the implementation to use the prefix Default instead of the suffix Impl.

The steps for writing rules using Detekt are as follows:

  1. Create a new module for the custom rule. We will call this moduledetekt-rules.
  2. Modify the build.gradle file on this module:
plugins {
    id 'kotlin'
}

dependencies {
    compileOnly "io.gitlab.arturbosch.detekt:detekt-api:$detektVersion"

    testImplementation "junit:junit:$junitVersion"
    testImplementation "org.assertj:assertj-core:$assertjVersion"
    testImplementation "io.gitlab.arturbosch.detekt:detekt-api:$detektVersion"
    testImplementation "io.gitlab.arturbosch.detekt:detekt-test:$detektVersion"
}

Copy the code
  1. Write rules to enforce the use of prefixes in all repository implementations (Default).

First, we need to extend the Rule class that Detekt provides for us. We also need to override the issue class members and specify the name, problem type, description, and time to resolve the problem.

Then override the visitClassOrObject function. Here we examine each implementation of each class. If some of them end with the keyword Repository, then we verify that the class name starts with a prefix. In this case, we’ll call the problem a bad taste of code.

class PrefixDefaultOnRepositoryRule(config: Config = Config.empty) : Rule(config) {

    companion object {
        const val PREFIX_REPOSITORY = "Default"
        const val REPOSITORY_KEYWORD = "Repository"
    }
    override val issue: Issue = Issue(
        javaClass.simpleName,
        Severity.Style,
        "Use the prefix Default on every 'XXXRepository' implementations.",
        Debt.FIVE_MINS
    )

    override fun visitClassOrObject(classOrObject: KtClassOrObject) {
        for (superEntry in classOrObject.superTypeListEntries) {
            if (superEntry.text.endsWith(REPOSITORY_KEYWORD) &&
                classOrObject.name?.contains(REPOSITORY_KEYWORD) == true &&
                classOrObject.name?.startsWith(PREFIX_REPOSITORY) == false
            ) {
                report(
                    classOrObject,
                    "The repository implementation '${classOrObject.name}' needs to start with the prefix 'Default'.")}}}private fun report(classOrObject: KtClassOrObject, message: String) {
        report(CodeSmell(issue, Entity.atName(classOrObject), message))
    }
}

Copy the code

The next steps are very similar to those in KTLint.

  1. Create a name calledCustomRuleSetProviderExtend theRuleSetProviderIn the class. And then I have to rewriteruleSetId() 和 instance(config: config)Function in which all rules are listed.
class CustomRuleSetProvider : RuleSetProvider {

    override val ruleSetId: String = "custom-detekt-rules"

    override fun instance(config: Config) =
        RuleSet(ruleSetId, listOf(PrefixDefaultOnRepositoryRule(config)))
}
Copy the code
  1. Create a file in the resources/ meta-INF /services folder. This file must contain the path of the class created in Step 4.

  1. Now in our project, we will add this module so that we can apply the rules. To use Detekt in your project, you also need a YAML-style configuration file. You can get the default configuration from the same Detekt repository by clicking here.
detekt {
    input = files("$rootDir/app/src")
    config = files("$rootDir/app/config/detekt.yml")
}

dependencies {
    detektPlugins "io.gitlab.arturbosch.detekt:detekt-cli:$detektVersion"
    
    detektPlugins project(path: ':detekt-rules')
    ...
}
Copy the code
  1. I also strongly recommend that you install this plug-in so that you can be notified of any errors in the same Android Studio project.

To view your custom rules in Android Studio, you need to generate a JAR from the module and add the path to the external Rulset JARs, as follows:

In this way! Now you can see that your custom rule has been applied 🎉

If you find any errors in the translation or other areas that need improvement, you are welcome to revise and PR the translation in the Gold Translation program, and you can also get corresponding bonus points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


Diggings translation project is a community for translating quality Internet technical articles from diggings English sharing articles. The content covers the fields of Android, iOS, front end, back end, blockchain, products, design, artificial intelligence and so on. For more high-quality translations, please keep paying attention to The Translation Project, official weibo and zhihu column.