AndroIdiots Podcast E18: Custom Lints with Hitanshu Dhawan

Anupam Singh
AndroIDIOTS
Published in
8 min readJul 20, 2020

Code Quality has always been a central theme in the life of a software developer.
Lints are one of the easiest ways to maintain some sanity over the codebase. Out of the box linters that Android Studio provides gives us the power to analyse the source code to flag programming errors, bugs, stylistic errors.

And in this episode, we have Hitanshu Dhawan from Urbanclap who helps us, deep-dive, into the same.

img src : preforce.com

Lints :

Lint, or a linter, is a tool that analyses source code to flag programming errors, bugs, stylistic errors, and suspicious constructs.

Eg :

Lint Error in Build.gradle
Lint Error in Xml
Lint Error in Kotlin/Java Code

Custom Lints :

In any organisation, there are internal code standards and Base classes like CustomViews which ought to be used instead of View classes provided by Android. Creating Custom Lints can ensure that all the other devs also follow the same standard and are warned if they miss something.

Use case: In our app as per the design guidelines all the radio button will have a default margin of 8 dp at the start which needs to be consistent throughout the app, and when a new guy tries to use the android version we want to show an error warning him/her about the custom option which is available

A new dev access the Android’s Radio Button, we want to show error message

Custom Radio Button Class:

class IdiotRadioButton : AppCompatRadioButton {
private var buttonPadding = 0

constructor(context: Context?) : super(context) {
init()
}

constructor(context: Context?, attrs: AttributeSet?) : super(
context,
attrs
) {
init()
}

constructor(
context: Context?,
attrs: AttributeSet?,
defStyleAttr: Int
) : super(context, attrs, defStyleAttr) {
init()
}

private fun init() {
buttonPadding = resources
.getDimensionPixelOffset(R.dimen.list_item_button_right_margin)
// default margin
}

override fun getCompoundPaddingLeft(): Int {
return buttonPadding + super.getCompoundPaddingLeft()
}
}

Step 1: Create a Java/Kotlin Library

In an existing android studio project create a new java/kotlin module lets name it “AndroidiotLint”

Step 2: Add dependencies

Module build.gradle

//AndroidiotLint/build.gradleapply plugin: 'java-library'

ext {
lintVersion = "26.5.3"
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])

// Lint
compileOnly "com.android.tools.lint:lint-api:$lintVersion"
compileOnly "com.android.tools.lint:lint-checks:$lintVersion"
// Lint Testing
testImplementation "com.android.tools.lint:lint:$lintVersion"
testImplementation "com.android.tools.lint:lint-tests:$lintVersion"
testImplementation "junit:junit:4.12"

}

app/ build.gradle

apply plugin: 'com.android.application'

dependencies {
//
existing dependencies
lintChecks project(path: ':AndroidiotLint')
}

Important: The lint version should correspond to the Android Gradle Plugin version + 23.If the AGP is 3.5.3, then the lint version should be 26.5.3

Step 3: Create Issue Registry

IssueRegistry class contains the central list of issues that the Linter references while checking the codebase

class IssueRegistry : IssueRegistry() {

override val issues: List<Issue>
get() = listOf(
IdiotCodeIssueDetector.ISSUE,
IdiotXmlIssueDetector.ISSUE
)
}

Step 4: Declare Issue Registry

Above step would be enough if we wanted the linter to only work with gradle. But the real power of custom Lints is how they work out of the box with Android Studio. This can be done by declaring Issue registry file path in

ModuleNam/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry

I.e Inside the Lint module only.

Create the directory structure and add the Issue registry path inside it

Step 5: Create a Custom Detector Class

A detector is able to find a particular problem. Each problem type is uniquely identified as an Issue.

class IdiotCodeIssueDetector : Detector() {
}

Step 6: Create an Issue

An issue is a potential bug in an Android application. An issue is discovered by a Detector, and has an associated Severity.

class IdiotCodeIssueDetector : Detector() {


companion object {

val ISSUE = Issue.create(
id = "IdiotRadioButtonUsageWarning",
briefDescription = "Android's RadioButton should not be used",
explanation = "Don't use Android Radio button, be an idiot and use IdiotRadioButton instead",
category = Category.CORRECTNESS,
priority = 3,
severity = Severity.WARNING,
implementation = Implementation(
IdiotCodeIssueDetector::class.java,
Scope.RESOURCE_FILE_SCOPE
)
)
}

}

Issue Attributes :

id = It’s a unique identifier for the issue and the one we mention in @SupressWarning(“”).

briefDescription = lines to explain the issue and will be shown for the lint, typically describing the problem rather than the fix.

category = A category is a container for related issues. It can be any one of the following type.

/** Issues related to running lint itself  */
@JvmField
val LINT = create("Lint", 110)

/** Issues related to correctness */
@JvmField
val CORRECTNESS = create("Correctness", 100)

/** Issues related to security */
@JvmField
val SECURITY = create("Security", 90)

/** Issues related to legal/compliance */
@JvmField
val COMPLIANCE = create("Compliance", 85)

/** Issues related to performance */
@JvmField
val PERFORMANCE = create("Performance", 80)

/** Issues related to usability */
@JvmField
val USABILITY = create("Usability", 70)

priority: It is defined on a scale of 1–10 which we can use to determine which issues to fix first.

severity: Determines if a lint would be treated as a warning or an error while building the app. An error will cause the build to break while a warning would be just printed to console or a file.

/**
* Fatal: Use sparingly because a warning marked as fatal will be
* considered critical and will abort Export APK etc in ADT
*/
FATAL("Fatal"),

/**
* Errors: The issue is known to be a real error that must be addressed.
*/
ERROR("Error"),

/**
* Warning: Probably a problem.
*/
WARNING("Warning"),

/**
* Information only: Might not be a problem, but the check has found
* something interesting to say about the code.
*/
INFORMATIONAL("Information"),

/**
* Ignore: The user doesn't want to see this issue
*/
IGNORE("Ignore");

implementation: Scope of the issue that it is interested in like manifest, resource files, java/Kotlin source files

Note : Issues and detectors are separate classes because a detector can discover
multiple different issues as it’s analysing code, and we want to be able to
different severities for different issues, the ability to suppress one but
not other issues from the same detector, and so on.

Step 7: Add the Scanner to the Detector

FileScanner : Allows to perform analysis on a file, based on subtype of scanner implemented in the detector.

Types of file scanners

As we want to scan the XML files to find the usage of RadioButton we will use XmlScanner, define the text reference we are looking for in an XML and the error message, and re-sync the Gradle afterward.

class IdiotCodeIssueDetector : Detector(), XmlScanner {


override fun getApplicableElements(): Collection<String> {
return listOf("RadioButton") // will look for Radio Button //Text in all xml
}

override fun visitElement(context: XmlContext, element: Element) {
context.report(
issue = ISSUE,
location = context.getNameLocation(element),
message = "Usage of RadioButton is prohibited" // Error message
)
}

Now if we open up the XML file we can see the error message

Step 8: Add Lint (Quick) Fix

As only showing error is not enough, we would ideally like to replace the Radio button with our IdiotRadioButton.

In comes LintFixes using which we provide structured data to be used by the IDE to create an actual fix.

class IdiotCodeIssueDetector : Detector(), XmlScanner {


override fun getApplicableElements(): Collection<String> {
return listOf("RadioButton")
}

override fun visitElement(context: XmlContext, element: Element) {

val idiotRadioButtonFix = LintFix.create()
.name("Use IdiotRadioButton")
.replace()
.text("RadioButton")
.with("com.androidiots.playground.IdiotRadioButton")
.robot(true)
.independent(true)
.build()

context.report(
issue = ISSUE,
location = context.getNameLocation(element),
message = "Usage of Radio Button is prohibited",
quickfixData = idiotRadioButtonFix
)
}
Now the error will give suggestion to use the IdiotRadioButton
RadioButton will be replaced by IdiotRadioButton

Testing Lints :

Unfortunately, we cannot debug our lints just like we debug apps. That’s because lints don’t run on mobile device. What we can do is writing test cases for the lint and run those tests in debug mode

class IdiotCodeIssueDetectorTest {


@Test
fun testRadioButton() {

lint()?.files(
xml(
"res/layout/layout.xml",
"""
<merge>
<RadioButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
</merge>
"""
).indented()
)
?.issues(IdiotCodeIssueDetector.ISSUE)
?.run()
?.expectWarningCount(1)
?.verifyFixes()
?.checkFix(
null,
xml(
"res/layout/layout.xml",
"""
<merge>
<com.androidiots.playground.IdiotRadioButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</merge>
"""
).indented()
)
}
}

This test checks that the actual number of errors in this lint check matches exactly the given count.

Lint Internals :

Java and kotlin files are converted to UAST and we write our lint rules for this UAST.

UAST stands for Universal abstract syntax tree, where it’s a tree representation of the source code. Read more about UAST here.

And along with gradlew and XML file, this UAST is consumed by the Lint Analysis process to point out lint errors.

References:

  • Link: Implementing your first Android lint rule
  • Link: Writing a Custom Android UI Inheritance Lint Rule
  • Link: Get started with Android Lint — Custom Lint Rules
  • Link: Android Lint Deep dive — Advanced custom Lint rules
  • Link: Building Custom Lint Checks in Android

We are inviting Android Developers to contribute back to the community via AndroIDIOTS. If you are interested in recording a podcast or writing tech articles, we would love to have you on board. Here is a small form you can fill so we can get back to you.

Follow us on Twitter for regular updates and feel free to DM the hosts (Anupam & Vivek) for any queries.

If you have any questions/suggestions/queries about the topic feel free to contact the speaker Hitanshu Dhawan for more info.

Cheers!!

--

--