Want to enforce specific coding standards in your Kotlin project? Custom lint rules let you tailor automated checks to your unique needs, ensuring code quality and consistency. Here's the quick breakdown:

  • Why Custom Lint Rules? Standard tools like Android Lint, ktlint, and Detekt catch common issues but fall short for project-specific requirements (e.g., naming conventions, security protocols).
  • Setup Essentials: Use Android Studio, Kotlin, and Gradle. Add dependencies like lint-api (Android Lint), ktlint-core, or detekt-api based on your chosen framework.
  • Rule Creation: Write logic using tools like Detector (Android Lint), Rule (ktlint), or Rule (Detekt) to flag violations.
  • Testing & Integration: Validate rules with testing libraries and integrate them into CI pipelines and IDEs for seamless enforcement.
  • Best Practices: Keep rules modular, document thoroughly, and update for Kotlin compatibility.

Custom linting isn't just about catching errors - it's about embedding your project's standards into every line of code. Let’s dive into how to set this up.

Setup Requirements and Environment

Required Tools and Dependencies

To begin creating custom lint rules, you’ll need specific tools and dependencies. Fortunately, most Kotlin developers already have the basics in place.

Android Studio is your go-to development environment, offering everything necessary for writing and debugging custom lint rules. Alongside this, you’ll need the Kotlin language and Gradle for build automation and dependency management.

The specific linting framework you choose will determine additional dependencies. For Android Lint, include the lint-api and lint-tests libraries in your build.gradle file. Use compileOnly for the API and testImplementation for testing libraries to avoid bloating your main application with unnecessary dependencies.

For ktlint, you’ll need to add the ktlint plugin to your build.gradle.kts and include the required dependencies for rule creation and testing. A key dependency here is com.pinterest:ktlint-core, which serves as the foundation for building custom rules.

If you’re using Detekt, add it as a dependency and configure your custom rules in the detekt.yml file. The primary dependency for this framework is io.gitlab.arturbosch.detekt:detekt-api.

To avoid compatibility problems, ensure that the versions of your lint framework, Kotlin, and Gradle align.

Once your dependencies are in place, you can move on to structuring your project for seamless integration of custom lint rules. Below is an example build.gradle configuration for Android Lint:

plugins {
  id 'java-library'
  id 'kotlin'
}
java {
  sourceCompatibility = JavaVersion.VERSION_1_8
  targetCompatibility = JavaVersion.VERSION_1_8
}
dependencies {
  implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
  compileOnly "com.android.tools.lint:lint-api:$lint_version"
  testImplementation "com.android.tools.lint:lint-tests:$lint_version"
  testImplementation 'junit:junit:4.13.2'
}
jar {
  manifest {
    attributes('Lint-Registry-v2': 'com.example.lint.CustomLintRegistry')
  }
}

This setup ensures your module is ready for developing and testing lint rules, with the manifest registration making your custom rules discoverable.

Project Structure Setup

A well-organized project structure is essential for maintaining and testing your custom lint rules effectively.

To keep things manageable, it’s best to create a dedicated module at the root level of your project, separate from your main application module. Name this module based on the framework you’re using, such as lint-rules, custom-ktlint-rules, or custom-detekt-rules. All your custom lint rule classes, configuration files, and test cases should reside in this module.

For Android Lint, the module should apply the java-library and kotlin plugins, set Java compatibility to version 1.8, and register your IssueRegistry in the JAR manifest. Ensure the minApi value in your custom Android Lint registry matches the version of your Android Gradle Plugin to avoid compatibility issues.

ktlint projects require an extra step: create a resources/META-INF/services directory to register your custom RuleSetProvider. This setup allows ktlint to automatically discover and apply your custom rules. You can even package your ruleset as a plugin for easy distribution across multiple projects.

For Detekt, the process involves adding your custom rule class to the ruleset provider and activating it in the detekt.yml configuration file.

Here’s a summary of the registration process for each framework:

Framework Module Setup Key Dependencies Registration Step
Android Lint lint-rules module com.android.tools.lint:lint-api Register IssueRegistry in manifest
ktlint custom-ktlint-rules com.pinterest:ktlint-core Register RuleSetProvider in META-INF
Detekt Custom ruleset module io.gitlab.arturbosch.detekt:detekt-api Register in detekt.yml and provider

Testing is a crucial part of the process. Use the appropriate testing libraries to verify your rules’ correctness. Organize your test directories to align with the framework you’re using.

Keep your dependencies up to date and watch for compatibility issues, particularly during major updates to linting frameworks or Kotlin itself. Many teams enforce strict version control and integrate lint rule testing into CI/CD pipelines to ensure smooth development.

Write your own Kotlin lint checks! | Tor Norbye

Kotlin

How to Write Custom Lint Rules for Kotlin

This section explains how to implement custom lint rules using Android Lint, ktlint, and detekt. These tools help enforce coding standards and maintain consistency across your Kotlin project. Each framework has a specific process for creating, registering, and integrating rules.

Creating Rules for Android Lint

Android Lint

Android Lint provides a powerful framework for defining custom rules that go beyond standard checks. To begin, create an IssueRegistry class in a dedicated lint module. This class acts as the central hub for your custom rules. Extend the IssueRegistry class and override the issues property to include your custom issues.

class CustomLintRegistry : IssueRegistry() {
    override val issues: List<Issue> = listOf(
        RxJavaNamingRule.ISSUE
    )

    override val minApi: Int = CURRENT_API
}

Next, define your custom rule by extending the appropriate detector class. For instance, to enforce naming conventions for methods, extend Detector and implement UastScanner. The rule uses the visitor pattern to analyze code and report violations.

class RxJavaNamingRule : Detector(), UastScanner {
    companion object {
        val ISSUE = Issue.create(
            id = "RxJavaNaming",
            briefDescription = "RxJava methods should follow naming conventions",
            explanation = "Methods returning Observable should end with 'Observable'",
            category = Category.CORRECTNESS,
            priority = 8,
            severity = Severity.WARNING,
            implementation = Implementation(
                RxJavaNamingRule::class.java,
                Scope.JAVA_FILE_SCOPE
            )
        )
    }

    override fun getApplicableMethodNames(): List<String>? = null

    override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
        val returnType = method.returnType?.canonicalText
        if (returnType?.contains("Observable") == true && !method.name.endsWith("Observable")) {
            context.report(
                ISSUE,
                node,
                context.getLocation(node),
                "Method returning Observable should end with 'Observable'"
            )
        }
    }
}

This method helps ensure code consistency and maintainability. Don’t forget to register your custom rules as outlined in the setup process.

Creating Rules for ktlint

ktlint

ktlint takes a different approach, focusing on code formatting and style. To create a custom rule, extend the Rule class and implement the visit method with your logic.

class NoAndroidLogRule : Rule("no-android-log") {
    override fun visit(
        node: ASTNode,
        autoCorrect: Boolean,
        emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
    ) {
        if (node.elementType == CALL_EXPRESSION) {
            val text = node.text
            if (text.contains("Log.d") || text.contains("Log.e") || 
                text.contains("Log.i") || text.contains("Log.w")) {
                emit(node.startOffset, "Android Log statements should be removed", false)
            }
        }
    }
}

Group your rules by creating a RuleSetProvider, which acts as a container for related rules.

class CustomRuleSetProvider : RuleSetProvider {
    override fun get(): RuleSet = RuleSet(
        "custom-rules",
        NoAndroidLogRule()
    )
}

To enable ktlint to recognize your rules, create a file at resources/META-INF/services/com.pinterest.ktlint.core.RuleSetProvider and reference your provider class. You can further configure these rules using .editorconfig files and include the custom rule module as a dependency in your project.

Creating Rules for detekt

detekt

Unlike ktlint, detekt focuses on broader code quality checks. Writing custom rules involves extending the Rule class and overriding the appropriate visit* function to analyze code and flag issues.

class TooManyParametersRule : Rule() {
    override fun visitNamedFunction(function: KtNamedFunction) {
        super.visitNamedFunction(function)

        val parameterCount = function.valueParameters.size
        if (parameterCount > 5) {
            report(
                CodeSmell(
                    issue,
                    Entity.from(function),
                    "Function ${function.name} has $parameterCount parameters, maximum allowed is 5"
                )
            )
        }
    }
}

Organize your rules by implementing a RuleSetProvider, which helps group them logically.

class CustomRulesetProvider : RuleSetProvider {
    override val ruleSetId: String = "custom-rules"

    override fun instance(config: Config): RuleSet = RuleSet(
        ruleSetId,
        listOf(
            TooManyParametersRule()
        )
    )
}

Activate your rules in the detekt.yml configuration file by setting active: true and adjusting parameters.

custom-rules:
  TooManyParametersRule:
    active: true
    maxParameters: 5

Real-World Example

In November 2022, Zee Palm developed custom lint rules for Qualoo to identify unlocalized strings in Flutter codebases. These rules helped extract and translate 300 app strings into Spanish, addressing a specific project need that standard tools couldn’t handle.

Choosing the right tool depends on your goals. Android Lint is ideal for in-depth code analysis, ktlint ensures formatting consistency, and detekt offers flexibility for broader quality checks.

sbb-itb-8abf120

Testing and Integration

Once you've implemented your custom lint rules, the next step is to ensure they're accurate and seamlessly integrated into your development workflow. Proper testing and integration are essential to make sure these rules provide real value in your projects.

Testing Your Lint Rules

Testing is crucial to confirm that your custom rules behave as expected. Most linting tools come with dedicated testing libraries to help you validate your rules. For Android Lint, you’ll need to include the following dependency in your project:

testImplementation "com.android.tools.lint:lint-tests:$lint_version"

You can then write JUnit tests to feed sample code snippets to your custom rule and verify that it detects violations. For example:

@Test
fun testDetectLogStatements() {
    val code = "fun foo() { Log.d(\"TAG\", \"message\") }"
    val findings = customRule.lint(code)
    assertTrue(findings.contains("Avoid using Log statements"))
}

If you're working with ktlint, its testing library allows you to create test cases to validate your rule's behavior against various code samples. Similarly, for Detekt, you can extend the Rule class and write tests to simulate code analysis and confirm accurate reporting.

In addition to unit tests, it's a good idea to run your custom rules on real projects to ensure they scale well with larger codebases. Integration tests are especially useful for catching edge cases that might not surface during unit testing. Be sure to profile the performance of your rules to avoid slowdowns during linting.

For Detekt users, keep in mind that rule modifications may require restarting the Gradle daemon using the --no-daemon flag. Double-check that your rules are active in the configuration files and that the correct module paths are set up.

Finally, make sure to integrate these tests into your build process to catch issues early.

Adding Rules to Development Workflows

To make your custom lint rules a part of daily development, integrate them into your Gradle build and CI pipelines. Add lint tasks - such as ./gradlew lint, ./gradlew detekt, or ktlint - to your CI build steps. Configure the pipeline to fail builds if lint violations are detected, preventing problematic code from being merged into your main branch.

IDE integration is another important step. This gives developers immediate feedback as they write code:

  • For Android Lint, custom rules are automatically detected if the lint rule module is properly included and registered in the project.
  • For ktlint, use the --apply-to-idea flag or relevant plugin tasks to integrate your custom rules into Android Studio or IntelliJ IDEA.
  • For Detekt, ensure the IDE plugin is installed and configured to recognize your custom ruleset.

Here’s a quick summary of how to integrate with different tools:

Tool Gradle Integration CI Pipeline Command IDE Setup
Android Lint Add module dependency; register IssueRegistry ./gradlew lint Automatic with proper registration
ktlint Include ruleset in dependencies ktlint Use --apply-to-idea flag
Detekt Add to detekt.yml, activate rules ./gradlew detekt Install IDE plugin; configure ruleset

To ensure a smooth transition, start with warning mode instead of failing builds immediately. This approach gives your team time to familiarize themselves with the new rules and fix existing violations without disrupting development. Once the team is comfortable and the codebase is clean, you can switch to error mode to enforce strict compliance.

Regular testing, both locally and in CI environments, helps catch issues early. You can also package your custom lint rules as separate modules or JARs, making them reusable across multiple projects. This modular approach allows you to share common rules across teams while still accommodating project-specific needs.

Best Practices and Maintenance

Creating custom lint rules is just the start. The bigger challenge is keeping them relevant and effective as your project evolves. By following some tried-and-true practices, you can ensure your rules remain useful and adaptable over time.

Writing Maintainable Rules

When designing lint rules, aim for a modular approach. Each rule should handle one specific task. This makes it easier to develop, test, and update individual rules without affecting the rest of your ruleset.

Naming is another key factor. Use names that clearly describe what the rule does. For example, instead of vague names like Rule1 or CustomCheck, go for something like NoHardcodedApiKeysRule or PreferDataClassOverClassRule. Clear names save your team time by making the purpose of each rule immediately obvious.

Documentation is equally important. Every rule should include details about its purpose, examples of compliant and non-compliant code, and any configuration options. This not only helps new team members onboard faster but also reduces the risk of misuse.

As your project grows, focus on performance. Target only the relevant parts of the code and avoid unnecessary deep AST traversals. Use caching for intermediate results where applicable, and profile your rules to identify any bottlenecks that could slow down builds on larger projects.

Lastly, make unit testing a core part of your rule development process. Test for a variety of scenarios, including edge cases. These tests not only ensure your rules work as expected but also act as a form of documentation, showing how the rules should behave.

By following these practices, you'll create rules that are easier to maintain and perform consistently, even as Kotlin evolves.

Updating Rules for New Kotlin Versions

Kotlin evolves quickly, and your lint rules need to keep up. Regular updates are essential to ensure compatibility with new language features, deprecations, and API changes.

Start by keeping an eye on Kotlin's release notes. They’ll alert you to any changes that could affect your rules. Make sure to also update your dependencies, including lint APIs, detekt, and ktlint. Running automated tests against new Kotlin versions can help you catch compatibility issues early.

To maintain flexibility, specify API version fields in your rules. This allows them to support both older and newer Kotlin features, reducing the risk of breaking projects that haven’t yet upgraded.

For smoother updates, consider a modular approach. Update individual rules incrementally rather than overhauling everything at once. This minimizes the chances of introducing breaking changes and makes it easier to roll back updates if something goes wrong.

Staying on top of updates ensures your lint rules remain aligned with Kotlin's progress, keeping your code quality efforts running smoothly.

How Expert Teams Like Zee Palm Use Custom Linting

Expert teams use custom linting to tackle challenges unique to their domains. Take Zee Palm, for example. With over 100 projects completed in fields like healthcare, AI, and blockchain, they rely on custom lint rules to maintain high-quality code in complex environments.

In healthcare applications, for instance, custom rules enforce strict naming conventions for patient data models and flag patterns that could expose sensitive data. In blockchain projects, specialized rules help identify security risks, such as reentrancy attacks or improper access controls in smart contracts.

AI and SaaS applications also benefit from custom linting. Rules can enforce architectural standards - like ensuring proper use of dependency injection - or validate that machine learning model inputs meet expected formats. These rules promote consistency across large, interconnected codebases with multiple contributors.

To make enforcement seamless, teams integrate these rules into CI/CD pipelines. This automates the process, reducing the burden of manual code reviews for style or standard violations. Many teams start by introducing new rules in a warning mode to give developers time to adjust. Once the rules are well understood, they switch to error mode. Regular audits of rule effectiveness ensure the linting system continues to provide value without slowing down development.

Conclusion

Creating custom lint rules for Kotlin can transform how you maintain code quality across your projects. It involves setting up tools, crafting logic using Android Lint, ktlint, or detekt, and seamlessly integrating these rules into your development workflow. While the initial setup takes effort, the long-term advantages make it worthwhile.

Custom linting offers tangible benefits. Teams that adopt automated linting with tailored rules report up to a 30% reduction in code review time and a 20% drop in post-release bugs. These gains are even more pronounced in specialized fields where code quality directly affects user safety or compliance with regulations. Such measurable outcomes highlight how automation can elevate your development process.

Automation plays a pivotal role here. As Zee Palm aptly puts it:

"You don't have to hire project managers, or expensive seniors to make sure others code well."

This kind of automation is indispensable in fast-paced environments where catching issues early can prevent costly delays and bugs. Custom lint rules ensure problems are identified during development, saving both time and resources.

For industries like healthcare or blockchain, the advantages go beyond error detection. Custom lint rules can enforce domain-specific requirements that generic tools might overlook. For instance, a fintech company in 2024 implemented custom ktlint rules to enhance secure logging practices, leading to a 40% reduction in security-related code issues within six months.

As your codebase grows, investing in custom linting becomes even more valuable. These rules not only uphold standards and catch errors but also ensure consistency throughout your projects. With regular updates to align with Kotlin's evolution, custom linting can become a cornerstone of your development infrastructure, maintaining quality without slowing down your team.

Start by addressing the most pressing issues and expand your ruleset as patterns emerge. Over time, your team - and your future self - will appreciate the consistency and reliability that custom linting brings to your Kotlin projects.

FAQs

What are the advantages of creating custom lint rules for your Kotlin project?

Custom lint rules in Kotlin provide customized code quality checks that cater to the unique needs of your project. They ensure adherence to coding standards, catch potential problems early, and encourage uniformity throughout your codebase.

Creating your own lint rules allows you to handle specific cases that generic linters might overlook - like enforcing project-specific architectural patterns or naming rules. This approach not only keeps your code easier to manage but also minimizes mistakes, ultimately saving both time and effort.

How can I make sure my custom lint rules stay compatible with future Kotlin versions?

To keep your custom lint rules working smoothly with future Kotlin updates, it's crucial to stick to best practices and keep an eye on Kotlin's evolution. Make a habit of checking Kotlin's release notes and official documentation to stay informed about updates that could impact your rules. Steer clear of hardcoding dependencies tied to specific Kotlin internals - opt for stable APIs instead whenever you can.

On top of that, make sure to thoroughly test your lint rules with every new Kotlin version. This proactive approach will help you catch and fix compatibility issues early. By staying on top of updates and being flexible in your approach, you can ensure your lint rules remain reliable as Kotlin continues to grow and change.

How can I seamlessly add custom lint rules to my CI/CD pipeline?

To include custom lint rules in your CI/CD pipeline effectively, you’ll first need to ensure the pipeline is set up correctly. Incorporate the custom lint rules into the build process, usually during the static code analysis stage.

Then, adjust your CI/CD tool to stop the build whenever linting issues are found. This step guarantees that code quality standards are automatically enforced. Afterward, conduct thorough testing to verify that the lint rules function consistently across all builds and environments.

Automating lint checks helps keep your codebase cleaner and allows you to catch potential issues early in development.

Related Blog Posts