DEV Community

Igor Wojda
Igor Wojda

Posted on

Introducing Konsist: A Cutting-Edge Kotlin Linter


Image description

Linters are vital tools in software development, helping to enforce code standards and best practices across the code base. By analysing code for errors and inconsistencies, linters improve collaboration, code quality, and adherence to coding standards, especially in complex projects.

Several linters are already available for Kotlin programming language. Rules for some linters may overlap e.g. both ktlint and detekt have a "Formatting Rule Set", however, each linter was created with a different focus in mind:

  • Ktlint - enforces Kotlin coding conventions. Its rules are mostly focused on code naming and code formatting (spacing, braces, empty blocks, line length, etc) .
  • Detekt -aimed at finding various code smells. Its rules are mostly focused on code smells, complexity, performance, and potential bugs.

Now you probably wonder why do we need another linter? Why do we need Konsist?

Well, let me tell you a short story…

Meet Jill, The Kotlin Developer

Image description

Jill had always been passionate about coding, and the opportunity to work as a Kotlin developer was a dream come true. She had just joined a thriving tech company, eager to contribute her skills and learn from her talented colleagues.

A few days into her role, Jill was assigned her first significant task: adding a new use case to the project. Excitement bubbled within her. This was a chance to prove herself, to make a meaningful impact. The task seemed straightforward enough, but she wanted to make sure she aligned her work with the established patterns of the codebase.

So, Jill began exploring the existing use cases, opening file after file, and digging into the structures and logic. She expected to find a consistent pattern, something that would guide her in crafting the new use case. But what she found was a bit overwhelming - every use case was different…

We all have been here - we join a new project and we need some time to figure out what is going on. Projects are often complex, and often this complexity is a direct result of an inconsistent, tangled and messy code base.

In the lifecycle of a project, as it scales and matures, and as developers come and go, integrating new features, addressing bugs, and applying patches usually compromise the code consistency.

The team can agree on codebase standards, but enforcing them is another story. Developers are not perfect, so it is quite easy to miss a thing or two while reviewing a PR.

Some standards are hard or impossible to guard using existing linters and thus many guards are usually not automated. Without automated guards, the codebase quality tends to degrade over time.

Both ktlint and detekt work on the file level - they are verifying files one by one, checking each file in isolation.

Meet Konsist

Konsist is a new cutting edge linter. Konsist focuses on capturing project-specific coding standards across similar types of code declarations. Let's explore a few high-level usages:

  • Every child class extending ViewModel must have "ViewModel" suffix
  • Classes with the @Repository annotation should reside in repository package
  • The presentation layer (defined by the presentation package) can only access classes from the domain layer (defined by the domain package)
  • Every use case constructor has alphabetically ordered parameters
  • Every repository constructor parameter has a name derived from the class name

Konsist provides two types of guards to protect the codebase - declaration guards and architectural guards.

Konsist Declaration Guards

Let's look at some practical applications. Let's review some code examples that demonstrate Konsist API and how it can be used to improve the quality of Kotlin code.

Consider this code snippet containing three declarations - com.myapp.logger package, logLevel property and FileLogger class:
package com.myapp.logger

internal const val logLevel = "debug"

@LoggerEntity
internal class FileLogger(val level: String) : BaseLogger {
   fun log(message: String) {

   } 
}
Enter fullscreen mode Exit fullscreen mode

There are a few things we can check in the above snippet. Let's start with getting the declaration names. The entry point of the Konsist linter is the Konsist class. For start lets query available declarations (of all types) and simply map their names:

Konsist
    .scopeFromProject() // create scope from all project files
    .declarations() // get declarations
    .map { it.name } // "com.myapp.logger", "loglevel", "FileLogger"
    ...
Enter fullscreen mode Exit fullscreen mode

The Konsist API can be mixed with known Kotlin collection processing API (the above map function) providing additional processing and filtering capabilities.

Mapping names will help us to understand how to retrieve various declarations and debug existing guards, however to define actual guards we must also verify whatever or not these declarations meet a certain criteria using assert function.

On a high-level Konsist declaration check contains 3 steps:

  1. Deciding what you want to check - e.g. classes…
  2. Querying the desired declarations - e.g. …extending BaseUseCase class…
  3. Defining an assertion to perform a check - e.g. …must have an internal modifier.

Let's start with checking if all classes extending a BaseLogger class (like above FileLogger) have a name ending withLogger:

Konsist
    .scopeFromProject()
    .classes()
    .withParentClassOf(BaseLogger::class)
    .assert { it.name.endsWith("Logger") }
Enter fullscreen mode Exit fullscreen mode

We can also verify if all classes present in logger package are annotated withLoggerEntity annotation:

Konsist
    .scopeFromProject()
    .classes()
    .resideInPackage("..logger..")
    .assert { it.hasAnnotations("LoggerEntity") }
Enter fullscreen mode Exit fullscreen mode

To make Konsist guard work we need to wrap above code in unit test - we will call such guard a Konstst test:

@Test
fun `classes present in logger package are annotated withLoggerEntity`() {
  Konsist
      .scopeFromProject()
      .classes()
      .resideInPackage("..logger..")
      .assert { it.hasAnnotations("LoggerEntity") }
}
Enter fullscreen mode Exit fullscreen mode

The @Test annotation is part of JUnit framework.

The test name provides description for the given guard, while self explanatory Konsist API allows to easily determine scope of the particular check.

Konsist Architecture Guards

The above examples demonstrate usage of the declaration guards. Konsist also provides a dedicated API for dependency verification that can assist you in guarding application layers. These guards ensure that a given class (which is part of a conceptual layer) can only use classes defined in a different package (another conceptual layer). Consider the following test, which guards the Clean Architecture layers of application residing in com.myapp package:

fun `clean architecture layers have correct dependencies`() {
      Konsist
          .scopeFromProduction()
          .assertArchitecture {
              // Define layers
              val domain = Layer("Domain", "com.myapp.domain..")
              val presentation = Layer("Presentation", "com.myapp.presentation..")
              val data = Layer("Data", "com.myapp.data..")

              // Define architecture assertions
              domain.dependsOnNothing()
              presentation.dependsOn(domain)
              data.dependsOn(domain)
          }
  }
Enter fullscreen mode Exit fullscreen mode

This guard is helpful when architectural layers are stored in packages rather than modules.

Take a read of extensive Konsist documentation and review snippets section for more examples.

Konsist API

Konsist API makes it easy for developers to define custom, concise, and expressive guards to check the correctness of the project code. Konsist can assert every declaration present in code - naming conventions, annotations, order and types of parameters, package structure, inheritance, modifiers, dependencies, architectural layers, and more.

Most linters work fine out of the box providing build-in rules, however, they usually don't scale well - writing a custom rule is often a complex task. It usually requires learning (not so obvious) internal linter extension API and simple conditions may result in quite verbose code. This is where Konsist shines - the Konsist API reflects declarations visible in Kotlin code, so API is much more intuitive for the developers. Konsist API mimics the structure of Kotlin code allowing it to quickly query Kotlin declarations such as files, classes, interfaces, functions, properties etc.

Konsist Project Status

Konsist library has been extensively tested on a variety of Spring and Android projects with both Gradle and Maven build systems. In addition, Konsist has a robust test suite (over 1300 tests and 20 unique CI checks) to prevent regression bugs.
Konsist is safe to use because it is not part of the production code base, but rather only used to verify the code base (similar to JUnit and other testing frameworks).

Konsist API has undergone multiple iterations to make it simpler to use and more versatile. At this stage, Konsist API should allow you to express most of the codebase guards you have in mind.

The Konsist project has reached the 2nd Milestone (Project Status page) and is actively seeking community feedback to help it mature:

Image description

Community Engagement

The Konsist project is now at a critical stage where community input is essential to polish it. Currently, the project needs external input and field testing to ensure that it is ready for widespread use. Please play with Konsist, try it out, and let us know what works, what features you would like to see, and what can be improved. Your feedback is crucial at this stage as it will help Konsist to mature.

If you find this project promising, you can show your support by starring it on GitHub, tweeting about it, or writing a blog post. This will help to raise awareness of the project, attract more contributors and mature this project faster.

Thank you for your support! 🙏

Summary

Konsist is a new, flexible linter that allows writing custom, project-specific rules to guard project code consistency. It accomplishes other linters offering a more familiar API that mimics Kotlin code declarations. Konsist fits into continuous integration pipelines and development workflows. For more information please review the Konsist documentation.

Follow me on Twitter(X).

Top comments (0)