Writing Custom Lint Rules in Android

Khayal Sharifli
ABB Innovation
Published in
4 min readJul 4, 2023

About

Hello, today we will talk about writing a custom lint rule. The first thing we need to know is why are we doing this, because Android Studio itself already has built-in lint rules and we already spend enough time writing code for the business side, so why waste time writing it?

  1. Why should we write custom lint?
  2. Write a custom lint rule with a real example.

Why should we write custom lint?

(In my current experience, this is the main reason I see for writing custom lint rules, others may have)
You have a large team and you have implemented certain rules for writing code within your company (for more readable and easy-to-understand code for everyone), but you don’t want to waste time on code reviews every time, and even if you look, you don’t want to waste time on code reviews to see if these rules are followed by developers. there will be moments, at this point writing custom lint comes into play and has a positive effect on both your time and your code. So let’s write a custom lint on a shared example.

Write a custom lint rule with a real example.

So now let’s write a custom lint rule with a real naming example and we’ll do it step by step.

  1. Let’s create a new module called lint-rules for custom lint rules:

2. Now we add implementations in lint-rules module’s build.gradle.

dependencies {
def lint_version = '30.1.0-alpha03'
compileOnly "com.android.tools.lint:lint-api:$lint_version"
testImplementation "com.android.tools.lint:lint-tests:$lint_version"
}

3. Let’s understand custom lint class terms “Visitor,” “Issue,” and “Detector”

3.1 Visitor: A visitor is a design pattern used to traverse and analyze the Abstract Syntax Tree (AST) of the source code. In the context of lint, a visitor is responsible for visiting different elements of the AST, such as classes, methods, expressions, or statements. It allows you to navigate the structure of the code and perform specific operations or checks on each visited element.

3.2 Issue: An issue represents a specific problem or rule violation that you want to detect in the codebase. It encapsulates information about the issue, including its ID, a short description, a detailed explanation, a category, and a priority level. Each issue can have associated configuration parameters, such as severity levels or additional options, to customize its behavior.

3.3 Detector: A detector is responsible for implementing the logic to identify and report issues in the codebase. It defines a set of visit methods that correspond to different AST elements, allowing you to perform checks and raise issues when certain conditions are met. The detector is registered with the lint framework, and during analysis, it is called by the visitor to inspect each element of the AST.

4. Create Method Naming Issue class

object MethodNamingIssue {
/**
* The fixed id of the issue
*/
private const val ID = "MethodNamingIssue"

/**
* The priority, a number from 1 to 10 with 10 being most important/severe
*/
private const val PRIORITY = 7

/**
* Description short summary (typically 5-6 words or less), typically describing
* the problem rather than the fix (e.g. "Missing minSdkVersion")
*/
private const val DESCRIPTION = "Wrong function name suffix."

/**
* A full explanation of the issue, with suggestions for how to fix it
*/
private const val EXPLANATION = """
Function is wrongly named.
"""

/**
* The associated category, if any @see [Category]
*/
private val CATEGORY = Category.CUSTOM_LINT_CHECKS

/**
* The default severity of the issue
*/
private val SEVERITY = Severity.WARNING

val ISSUE = Issue.create(
ID,
DESCRIPTION,
EXPLANATION,
CATEGORY,
PRIORITY,
SEVERITY,
Implementation(
MethodNamingDetector::class.java,
Scope.JAVA_FILE_SCOPE
)
)

class MethodNamingDetector : Detector(), Detector.UastScanner {
override fun getApplicableUastTypes(): List<Class<out UElement>> =
listOf(UMethod::class.java)

override fun createUastHandler(context: JavaContext): UElementHandler =
MethodNamingVisitor(context)
}
}

5. Create Visitor class

@Suppress("UnstableApiUsage")
class MethodNamingVisitor(private val context: JavaContext) : UElementHandler() {
override fun visitMethod(node: UMethod) {
if (node.returnClassName() == "String") {
if (!node.name.endsWith("Once")) {
reportIssue(node)
}
}
}

private fun UMethod.returnClassName(): String =
(returnTypeReference?.type as? PsiClassType)?.className ?: ""

private fun reportIssue(node: UMethod) {
context.report(
issue = MethodNamingIssue.ISSUE,
scopeClass = node,
location = context.getNameLocation(node),
message = """
[String] string parameters must have a Once key at the end.
Example: removeAccountOnce()
"""
)
}
}

6. Now we need register our custom lints for this we create Registry class

@Suppress("UnstableApiUsage")
class CustomLintRegistry : IssueRegistry() {
override val issues =
listOf(
MethodNamingIssue.ISSUE
)

override val api: Int = CURRENT_API

override val minApi: Int = 6
}

7. Add this line in lint-rule’s build.gradle (Don’t forget to change direction)

jar {
manifest {
attributes('Lint-Registry-v2': 'az.khayalsharifli.lint_rules.CustomLintRegistry')
}
}

8. Add lint-rules module in our main module

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
// Lint module
lintChecks project(':lint-rules')
}

9. Now only write “./gradlew lint” in Android Studio terminal

10. And this is our custom lint worked

Finally, I hope this was helpful for you, you can view all the code by linking to my Github Repository.

--

--