Lin is an Android Lint tool made simpler. It has two different goals:

  1. To create a set of highly opinionated detectors to apply to your Android projects.
  2. To offer a Kotlin DSL to write your own detectors in a much easier way.

How to use

Add the JitPack repository to your build file:

allprojects {
    repositories {
        maven { url '' }

Lin - Detectors

Add the detectors module dependencies to your project and the dsl module as part of the lint classpath:

dependencies {
    lintChecks 'com.github.serchinastico.lin:detectors:0.0.3'
    lintClassPath 'com.github.serchinastico.lin:dsl:0.0.3'

Lin - DSL (Domain Specific Language)

If you want to write your own detectors just add the dsl and annotations modules to your linting project:

dependencies {
    compileOnly 'com.github.serchinastico.lin:dsl:0.0.3'
    compileOnly 'com.github.serchinastico.lin:annotations:0.0.3'

How to write your own detectors

Lin offers a DSL (Domain Specific Language) to write your own detectors easily. The API is focused on representing your rules as concisely as possible. Let's bisect an example of a detector to understand how it works:

fun noElseInSwitchWithEnumOrSealed() = detector(
    // Define the issue:
        // 1. What files should the detector check
        // 2. A brief description of the issue
        "There should not be else/default branches on a switch statement checking for enum/sealed class values",
        // 3. A more in-detail explanation of why are we detecting the issue
        "Adding an else/default branch breaks extensibility because it won't let you know if there is a missing " +
                "implementation when adding new types to the enum/sealed class",
        // The category this issue falls into
) {
    /* The rule definition using the DSL. Define the
     * AST node you want to look for and include a
     * suchThat definition returning true when you want 
     * your rule to report an issue.
     * The best way to see what nodes you have
     * available is by using your IDE autocomplete
     * function.
    switch {
        suchThat { node ->
            val classReferenceType = node.expression?.getExpressionType() ?: ([email protected] false)

            if (!classReferenceType.isEnum && !classReferenceType.isSealed) {
                [email protected] false

            node.clauses.any { clause -> clause.isElseBranch }


You can specify your rules using quantifiers, that is, numeric restrictions to how many times you are expecting a specific rule to appear in order to be reported.

fun noMoreThanOneGsonInstance() = detector(
        "Gson should only be initialized only once",
        """Creating multiple instances of Gson may hurt performance and it's a common mistake to instantiate it for
            | simple serialization/deserialization. Use a single instance, be it with a classic singleton pattern or
            | other mechanism your dependency injector framework provides. This way you can also share the common
            | type adapters.
    // We can use anyOf to report if any of the rules
    // included is found.
        // This rule will only report if more than one
        // file has any call expression matching the 
        // suchThat predicate.
        file(moreThan(1)) { callExpression { suchThat { it.isGsonConstructor } } },
        // On the other hand, this rule will only 
        // report if there is any file with more than
        // one call expression matching the suchThat
        // predicate.
        file { callExpression(moreThan(1)) { suchThat { it.isGsonConstructor } } }

The list of available quantifiers is:

val any                  // The default quantifier, if a rule matches any number of times then it's reported
val all                  // It should appear in every single appearance of the node
fun times(times: Int)    // Match the rule an exact number of "times"
fun atMost(times: Int)   // matches <= "times"
fun atLeast(times: Int)  // matches >= "times"
fun lessThan(times: Int) // matches < "times"
fun moreThan(times: Int) // matches > "times"
val none                 // No matches


Lin detectors can store and retrieve information from a provided map. This is really useful if you have dependant rules where one of them might depend on the value of another, e.g. an activity class name having the same name as the layout it renders.

Because Lin uses backtracking on the process of finding the best match for rules it's highly discouraged to store information by yourself, intead you should use the storage property provided in the suchThat block.

    import {
        suchThat { node ->
            val importedString = node.importReference?.asRenderString() ?: [email protected] false
            val importedLayout = KOTLINX_SYNTHETIC_VIEW_IMPORT
                ?.value ?: [email protected] false
            // Here we have access to the LinContext object that holds
            // a reference to a map of values where you can store
            // string values.
            params["Imported Layout"] = importedLayout

    expression {
        suchThat { node ->
            // We retrieve the information we stored previously
            // It's the same value we stored when the rule returned
            // true so we are sure it's the one we need.
            val importedLayout = params["Imported Layout"] ?: [email protected] false
            val usedLayout = LAYOUT_EXPRESSION.matchEntire(node.asRenderString())
                ?.value ?: [email protected] false
            return usedLayout != importedLayout

The storage property is just a MutableMap<String, String>. The matching algorithm takes care of keeping the map in a coherent state while doing the search so that you won't find values stored in failing rules. All siblings and child nodes will see stored values.

It's also important to keep in mind that Lin will try to match rules in any order. The most important implication is that even if you define a rule in a specific order Lin might find matches in the opposite:

    expression {
        suchThat {
            storage["node"] = it.asRenderString() 

    expression {
        suchThat { "MyExpression" == storage["node"] }

Even if the expression storing things in the storage is defined before, that order is not honored when looking for the best match of rules, so it might happen that storage["node"] is null.


Show the world you're using Lin.

Lint tool: Lin

[![Lint tool: Lin](](