“This article has participated in the good article call order activity, click to see: back end, big front end double track submission, 20,000 yuan prize pool for you to challenge!”

In the previous three articles, we have described the importance of static code scanning in teams and how to use Gitlab CI/CD in conjunction with static code scanning implementations to get team members to follow code specifications with low awareness in a real team practice. In practice, we only used ktLint to check Kotlind’s official code style specifications, but in practice, we will have more team code specifications, such as uniform log printing methods, each activity file must have comments, and so on. So, as a concluding article on Android static code scanning practices, I’ll show you how to write custom rules using KtLint.

Ktlint Loads the process of rules

While there are simple official documents that teach you how to customize ktLint rules, I find it helpful to know how to load the rules after executing./gradlew ktlint.

First, find where the ktLint gradle task is defined. It’s in build.gradle in the app directory at the root of the project

.configurations {
    ktlint
  }
  ...
  task ktlint(type: JavaExec, group: "verification") {
    description = "Check Kotlin code style."
    classpath = configurations.ktlint
    main = "com.pinterest.ktlint.Main"
    args "-a"."src/**/*.kt"."--reporter=html,output=${buildDir}/ktlint.html"}...dependencies{... ktlint("Com. Pinterest: ktlint: 0.41.0.") {
        attributes {
            attribute(Bundling.BUNDLING_ATTRIBUTE, getObjects().named(Bundling, Bundling.EXTERNAL))
        }
    }
    ...
  }
Copy the code

One defines the name for ktlint gradle task, type of JavaExec, execution will execute the Java application in the child process (Jar), classpath defines the path to execute the Jar, And configurations. The ktlint is a defined called ktlint reference set, in which only refers to the “com. Pinterest: ktlint: 0.41.0”, later you can add your own jars. Main said to execute the main method for com. Pinterest. Ktlint. Main.

So we can see directly in ktlint source com. Pinterest. Ktlint. The Main method

We run./gradlew ktlint without the laced command, so we go straight to the next ktlintcommand-run () method.

Which failOnOldRulesetProviderUsage () is to determine whether a use the Jar have inherited the old rules method, if you have, error directly. The next step is how to load the rules.

val ruleSetProviders = rulesets.loadRulesets(experimental, debug, disabledRules)
Copy the code

Enter the loadRulesets method again

You can see that all the classes in the Jar that implement the RuleSetProvider abstract class are loaded, along with some filters, And RuleSetProvider abstract class the get method returns a series of com. Pinterest. Ktlint. Core. The Rule of the abstract class rules, on the back of the steps is interested, you can go to see the ktlint source, Here we just need to understand the process of loading the rules. A rough summary is as follows:

So our custom rule is to create a custom class that implements the RuleSetProvider abstract class, export it as a Jar, and reference your Jar with ktLint in the app directory build.gradle at the project root.

Program Structure Interface (PSI)

We need to define a class that implements the RuleSetProvider abstract class that returns our own set of rules. And rules is an implementation of com. Pinterest. Ktlint. Core. The abstract class Rule, the rules of the implements of visit abstract methods in class, in this way, we are going to complete identification is not in conformity with the specification of a code block and output the function of warning that text, The ASTNode parameter to this abstract method is the key to identifying the code block.

ASTNode is one of the classes in the PSI (Program Structure Interface), JetBrains’ implementation of the IDE’s Abstract Syntax Tree (AST). To express the programming language in the form of a tree, we programmers write source code syntax structure for abstract representation. PSI translates code written by programmers into a tree structure that facilitates code parsing.

We can use the PsiViewer plug-in to visually view the tree structure generated by PSI. The following two diagrams can visually see the use of the plug-in and the display of the tree structure:

Implement your first custom KtLint rule

Talk is cheap. Show me the code. So I’m using a custom ktLint rule — you can’t directly inherit Activity(), you must inherit BaseActivity implementations — as an example. Hopefully you’ll get an idea of how to implement a custom ktLint rule. For debugging purposes, the following example is done under a available Android project, which makes it easier for us to debug and, when completed, migrate to a separate Kotlin project for distribution, such as the sample code on Github.

Create a custom KtLint rule module

  • Create a separate module in the project root folder at the same folder level as the app module. Here I’ll name the module custom_rules;

  • Will create the module underbuild.gradleThe file is modified as follows, where the dependent kotlin version number is the same as the project root directorybuild.gradleThe file version is consistent:
plugins {
    id 'kotlin'
}

compileKotlin {
    kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
    kotlinOptions.jvmTarget = "1.8"
}

dependencies {
    implementation "Org. Jetbrains. Kotlin: kotlin - stdlib: 1.5.20"
    compileOnly "Com. Pinterest. Ktlint: ktlint - core: 0.41.0"
}
Copy the code
  • Tell ktLint to find our implementationRuleSetProviderUnder the new modulesrc->mainLet’s create a new folderresources/META-INF/servicesAnd create a new one in the directorycom.pinterest.ktlint.core.RuleSetProviderFile, add to the file
com.tc.custom_rules.CustomRuleSetProvider
Copy the code

At this time, our file directory is as follows:

Create a new rule class to implement rules

  • newExtendBaseRuleClass implementsRuleAbstract class, where id is convenient for us to find the filter rule, ASTNode can refer to the relatedIDEA Program Structure Interface (PSI) official reference documentAnd combined with thePsiViewer plug-inCan we figure out how to filter nonconforming Kotlin files
package com.tc.custom_rules

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType
import com.pinterest.ktlint.core.ast.children
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes

class ExtendBaseRule : Rule("kclass-extend-base-rules") {
    override fun visit(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int.errorMessage: String.canBeAutoCorrected: Boolean) - >Unit
    ) {
        if (node.elementType == KtStubElementTypes.CLASS) {
            println("Using debug to print logs:${node.text}")
            // ASTNode with class modifier
            var isExtendActivity = false
            // Determine whether the class inherits the Activity
            for (childNode in node.children()) {
                if (childNode.elementType == KtStubElementTypes.SUPER_TYPE_LIST) {
                    // Psi inheriting and implementing classes
                    for (minChild in childNode.children()) {
                        if (minChild.elementType == KtStubElementTypes.SUPER_TYPE_CALL_ENTRY) {
                            // Psi class to determine the text of the inherited ASTNode
                            if (minChild.text == "Activity()") {
                                isExtendActivity = true
                            }
                            break}}}}if (isExtendActivity) {
                // If the class inherits the Activity, determine if it is BaseActivity
                for (childNode in node.children()) {
                    if (childNode.elementType == ElementType.IDENTIFIER) {
                        // The first identifier is the class name
                        if(isExtendActivity && childNode.text ! ="BaseActivity") {
                            // The class is inherited from the Activity and is not BaseActivity, so the output is incorrect
                            emit(
                                childNode.startOffset,
                                "Activity inherits BaseActivity!".false
                            )
                            break
                        }
                        break
                    }
                }
            }
        }
    }
}
Copy the code
  • CustomRuleSetProviderClass implementsRuleSetProviderReturns the rule defined above
package com.tc.custom_rules

import com.pinterest.ktlint.core.RuleSet
import com.pinterest.ktlint.core.RuleSetProvider

class CustomRuleSetProvider : RuleSetProvider {
    override fun get(a): RuleSet = RuleSet(
        "custom-rule-set",
        ExtendBaseRule()
    )
}
Copy the code

Used with ktLint

  • inappThe modulebuild.gradleRely on the module
. dependencies { ... ktlint project(':custom_rules') }Copy the code
  • And then terminal execution./gradlew ktlintYou can see that our custom rules have taken effect

Export the JAR for custom rules

In practice, it is not possible to add a custom rule module every time there is a new project configuration rule, so we need to export the custom rule module as a JAR for the Android project to reference.

You can do this on top of the custom rule module you just created

./gradlew :custom_rules:build
Copy the code

Or you can just execute the custom module as a separate Kotlin project

./gradlew build
Copy the code

You can see the built JAR in build->lib, after which you can publish it to the Maven repository.

conclusion

Having shown you how to implement custom rules, this series is almost complete with team static code specification practices based on KtLint and Gitlab CI/CD. If my article is helpful or inspiring to you, please give me a thumbs-up 👍🏻 and support me. If there is a mistake, welcome to the leaders to correct, but also welcome to discuss, thank you.

Reference documentation

Gradle Reference Document Writing your First KtLint rule — Niklas Baudy IDEA Program Structure Interface (PSI) official reference document Custom rule example code Github