Only this pageAll pages
Powered by GitBook
1 of 66

Konsist

GETTING STARTED

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

WRITING TESTS

Loading...

Loading...

Loading...

Loading...

Loading...

VERYFYING CODEBASE

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

FEATURES

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

INSPIRATION

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

ADVANCED

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

HELP

Loading...

Loading...

Loading...

Loading...

OTHER

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Getting Started

The following example provides the minimum setup for defining and running a single Konsist test.

Check the starter projects containing Konsist tests or review the Konsist API reference.

At a high-level Konsist check is a Unit test following multiple implicit steps.

3 steps are required for a declaration check and 4 steps are required for an architecture check:

The declaration represents Kotlin declaration eg. Kotlin class is represented by KoClassDeclaration allowing to access class name (koClassDeclaration.name), methods (koClassDeclaration.functions()), etc. See .

Declaration

What is Konsist?

Konsist is a structural linter (static code analyzer) designed for Kotlin language. Verifying codebase with Konsist enables development teams to enforce architectural rules and class structures through automated testing.

Konsist offers comprehensive verification capabilities that enable developers to enforce architectural rules and maintain code consistency, thereby improving the readability and maintainability of the code. It's like ArchUnit, but for Kotlin language. Whether you're working on Android, Spring, or Kotlin Multiplatform projects, Konsist has got you covered.

The Konsist API provides developers with the capability to create custom checks through unit tests, customized to align with the project's unique requirements. Additionally, it offers smooth integration with leading testing frameworks, including JUnit4, JUnit5, and Kotest, further streamlining the development process.

Konsist is approaching its 1.0 release, marking a significant milestone in its development journey. See the .

Konsist offers two types of checks, namely and , to thoroughly evaluate the codebase.

Declaration Checks

The first type involves declaration checks, where custom tests are created to identify common issues and violations at the declaration level (classes, functions, properties, etc.). These cover various aspects such as class naming, package structure, visibility modifiers, presence of annotations, etc. Here are a few ideas of things to check:

  • Every child class extending ViewModel must have ViewModel suffix

  • Classes with the @Repository annotation should reside in ..repository.. package

  • Every class constructor has alphabetically ordered parameters

Here is a sample test that verifies if every use case class resides in domain.usecase package:

For more Konsist test samples see the section.

ArchitecturalChecks

The second type of revolves around architecture boundaries - they are intended to maintain the separation of concerns between layers.

Consider this simple 3 layer of :

  • The domain layer is independent

  • The data layer depends on domain layer

  • The presentation layer depends on domain

Here is a Konsist test that verifies if Clean Architecture dependency requirements are valid:

These types of checks are useful when the architecture layer is defined by the package, rather than a module where dependencies can be enforced by the build system.

Summary

By utilizing Konsist, teams can be confident that their Kotlin codebase remains standardized and aligned with best practices, making code reviews more efficient and code maintenance smoother.

Add Konsist Dependency

Add Maven Central Repository

Add mavenCentral repository:

Add Konsist Dependency

Known Issues

  • Every constructor parameter has a name derived from the class name

  • Field injection and m prefix is forbidden

  • Every public member in api package must be documented with KDoc

  • and more...

  • layer
  • etc.

  • Project Status
    Snippets
    Konsist checks
    Clean Architecture
    Declaration Checks
    ArchitecturalChecks
    To use Konsist, include the Konsist dependency from Maven Central:

    Add the following dependency to the module\build.gradle.kts file:

    dependencies {
        testImplementation("com.lemonappdev:konsist:0.17.3")
    }

    Add the following dependency to the module\build.gradle file:

    dependencies {
        testImplementation "com.lemonappdev:konsist:0.17.3"
    }

    Add the following dependency to the module\pom.xml file:

    <dependency>
        <groupId>com.lemonappdev</groupId>
        <artifactId>konsist</artifactId>
        <version>0.17.3</version>
        <scope>test</scope>
    </dependency>

    See Compatibilityto learn how Konsist integrates with Kotlin ecosystem.

    To achieve better test separation Konsist can be configured inside a customkonsistTest source set or a dedicated konsistTest module. See Isolate Konsist Tests for guidelines on how to store Konsist test in project codebase and how to run them using cmd.

    class UseCaseKonsistTest {
        @Test
        fun `every use case reside in use case package`() {
            Konsist
                .scopeFromProject() // Define the scope containing all Kotlin files present in the project
                .classes() // Get all class declarations
                .withNameEndingWith("UseCase") // Filter classes heaving name ending with 'UseCase'
                .assertTrue { it.resideInPackage("..domain.usecase..") } // Assert that each class resides in 'any domain.usecase any' package
        }
    }
    
    class UseCaseKonsistTest : FreeSpec({
        "every use case reside in use case package" {
            Konsist
            .scopeFromProject() // Define the scope containing all Kotlin files present in the project
            .classes() // Get all class declarations
            .withNameEndingWith("UseCase") // Filter classes heaving name ending with 'UseCase'
            .assertTrue (
                    testName = this.testCase.name.testName
             ){ 
                  it.resideInPackage("..domain.usecase..") 
             } // Assert that each class resides in 'any domain.usecase any' package
        }
    })
    class ArchitectureTest {
        @Test
        fun `clean architecture layers have correct dependencies`() {
            Konsist
                .scopeFromProject() // Define the scope containing all Kotlin files present in project
                .assertArchitecture { // Assert architecture
                    // 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)
                }
        } 
    }
    class ArchitectureTest : FreeSpec({
        "every use case reside in use case package" {
            Konsist
                .scopeFromProject() // Define the scope containing all Kotlin files present in project
                .assertArchitecture { // Assert architecture
                    // 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)
                }
        }
    })
    repositories {
        mavenCentral()
    }

    Assets And Logos

    The Konsist logo can be found in the of the main repository.

    Our in JIRA. If you want to join say hello at Slack channel. If you are working on a ticket make sure to get the JIRA access, assign it to yourself, and update the ticket status to In Progress).

    misc folder
    contributor backlog is public
    #konsist-dev

    Snippets

    In Konsist all checks are tailored for a given project.

    As the project grows additional checks can be defined to enforce various checks eg. layer boundaries, package structure, class naming, and more. The following sections contain a set of sample checks to give an idea of what is achievable with Konsist.

    Most of the snippets are wrapped as JUnit tests, however, these snippets can be easily wrapped in the kotest tests.

    Compatibility

    Konsist ecosystem compatibility

    Konsist is compatible with all types of Kotlin projects including Android, Spring, and Kotlin Multiplatform projects.

    Konsist works with popular testing frameworks executing Kotlin code. Konsist has first-class support for JUni4, JUnit5, and Kotest.

    Konsist is compatible with popular build systems such as Gradle and Maven.

    The Java 8 is a minimum Java version required to run Konsist.

    Konsist is backwards compatible with Kotlin 1.8.x ( since Konsist 0.17.0 ).

    Dependencies

    Konsist depends on:

    • org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.4 (minimal coroutine usage make Konsist compatible with newer coroutines versions)

    • org.jetbrains.kotlin:kotlin-compiler-embeddable:2.0.20

    Contributors

    Browse the current list of contributors directly on GitHub.

    When Konsist API Is Not Enough

    Getting Help

    📢 Let us know about issues you face and improvements you would like to see. Your feedback is crucial in shaping the future of Konsist. Whether it's a suggestion for improvement, a bug report, or a question about usage, we're here to listen and help.

    Share your thoughts on the #konsist channel to get help or start a new GitHub discussion 💬 for bug reports, issues, and feature requests.

    Package Wildcard

    Select packages

    Package wildcard syntax is used to provide a more flexible way of querying packages.

    The two dots (..) means any zero or more packages eg. all classes reside in a package starting with com.app:

        Konsist
            .scopeFromProject()
            .classes()
            .assertTrue { it.resideInPackages("com.app..") }
            
    // com.app.data - valid  
    // com.app.data.repository - valid  
    // com.data - invalid
    // com - invalid
            

    Package wildcard syntax can be used multiple times inside the string argument. Here all interfaces reside in a package logger prefixed and suffixed by any number of packages:

        Konsist
            .scopeFromProject()
            .interfaces()
            .assertTrue { it.resideInPackages("..logger..") }
    
    // logger - valid  
    // com.logger - valid  
    // com.logger.tree - valid
    // com - invalid

    Additional JUnit5 Setup

    By default, JUnit tests are run sequentially in a single thread. To speed up tests parallel execution can be enabled.

    Create junit-platform.properties a file containing:

    junit.jupiter.execution.parallel.enabled=true
    junit.jupiter.execution.parallel.mode.default=concurrent
    junit.jupiter.execution.parallel.config.strategy=dynamic
    junit.jupiter.execution.parallel.config.dynamic.factor=0.95

    Place this file in the resources directory of the test source set e.g:

    src/test/resource/junit-platform.properties
    
    or
    
    src/konsistTest/resource/junit-platform.properties

    Read more in the official JUnit5 documentation.

    Articles & Videos

    Videos

    1. Maintain consistency in your Kotlin Codebase with Konsist!

    2. Harmonizing Kotlin codebase with Konsist (KotlinConf 2024)

    3. (FR)

    Articles

    Generic Types Snippets

    1. All Generic Return Types Contain X In Their Name

    @Test
    fun `all generic return types contain X in their name`() {
        Konsist
            .scopeFromProduction()
            .functions()
            .returnTypes
            .withGeneric()
            .assertTrue { it.hasNameContaining("X") }
    }

    2. Property Generic Type Does Not Contains Star Projection

    3. All Generic Return Types Contain Kotlin Collection Type Argument

    4. Function Parameter Has Generic Type Argument With Name Ending With Repository

    java.lang.OutOfMemoryError: Java heap space

    For large projects with many classes to parse, the default JVM heap size might not suffice. If you encounter java.lang.OutOfMemoryError: Java heap space error consider increasing the maxHeapSize for the test source set:

    Add the following argument to thebuild.gradle.kts file:

    tasks.withType<Test> {
        maxHeapSize = "1g"
    }

    Add the following argument to the build.gradle file:

    tasks.withType(Test).configureEach { 
        maxHeapSize = "1g" 
    }

    Add the following argument to the pom.xml file:

    You may need to set larger value than 1 gigabyte.

    Verify Source Declarations

    The source declaration (sourceDeclaration property) holds the reference to actual type declaration such as class or interface.

    Konsist API allows for verify properties of such type e.g.:

    • Check if property type implements certain interface

    • Check if function return type name ends with Repository

    • Check if parent class is annotated with given annotation

    Every declaration that is using another type such as property, function, parent exposes sourceDeclaration property.

    Let's look at few examples:

    Verify Property Source Declaration

    Check if type of current property is has a type which is a class declaration heaving internal modifier:

    Note that explicit casting (asXDeclaration) has to be used to access specific properties of the declaration.

    Verify Function Return Type Source Declaration

    Check if function return type is a basic Kotlin type:

    Verify Class Has Interface Source Declaration

    Kotlin Serialization Snippets

    Konsist can be used to guard the consistency of classes related to the [Kotlin Serialization](https://kotlinlang. org/docs/serialization.html) library.

    1. Classes Annotated With Serializable Have All Properties Annotated With SerialName

    @Test
    fun `classes annotated with 'Serializable' have all properties annotated with 'SerialName'`() {
        Konsist
            .scopeFromProject()
            .classes()
            .withAnnotationOf(Serializable::class)
            .properties()
            .assertTrue {
                it.hasAnnotationOf(SerialName::class)
            }
    }

    2. Enum Classes Annotated With Serializable Have All Enum Constants Annotated With SerialName

    3. All Models Are Serializable

    Indirect Parents

    The indirectParents parameter (parents(), hasParentClass(), hasAllParentInterfacesOf methods etc.). specifies whether or not to retrieve parent of the parent (indirect parents). By default, indirectParents is false e.g.

    For above inheritance hierarchy is possible to retrieve:

    1. Direct parents of ClassC (ClassB):

    1. All parents present in the codebase hierarchy (ClassB and ClassC):

    Notice that only parents existing in the project code base are returned.

    Declaration Vs Property

    Some code constructs can be represented as declarations (declaration-site) and as properties (use-site).

    Declaration Site

    Consider this annotation class:

    The above code represents the declaration of the CustomLogger annotation class, the place in the code where this annotation is declared (declaration-site). This declaration can be retrieved by filtering KoScope declarations...

    Kotest Support

    Konsist + Kotest

    Konsist has first-class support for meaning that every following release will be developed with Kotest compatibility in mind. API has been improved to support Kotest flows. However, Konsist cannot automatically retrieve Kotest test names, meaning the test name won't appear in error logs upon test failure. To fully utilize Konsist with Kotest, you must explicitly provide the test name.

    Setting The Test Name

    Konsist can't obtain the test name from all dynamic tests (including tests).

    It's recommended to provide the test name using the testName parameter. Supplying a test name provides additional benefits:

    Declaration

    What is declaration?

    The declaration (KoDeclaration) represents a code entity, a piece of Kotlin code. Every parsed Kotlin File (KoFileDeclaration) contains one or more declarations. The declaration can be a package (KoPackageDeclaration), property (KoPropertyDeclaration), annotation (KoAnnotationDeclaration), class (KoClassDeclaration), etc.

    Consider this Kotlin code snippet file:

    The above snippet is represented by the KoFileDeclarationclass. It contains two declarations - property declaration (KoPropertyDeclaration

    Library Snippets

    Snippets to library authors.

    1. Every Api Declaration Has KDoc

    2. Every Function With Parameters Has A Param Tags

    Test Snippets

    1. Every Class Has Test

    2. Every Class - Except Data And Value Class - Has Test

    Kotest Snippets

    Sample tests writen using library.

    1. Use Case Test

    2. Use Case Tests

    Architecture Snippets

    Snippets used to guard application architecture.

    1. 2 Layer Architecture Has Correct Dependencies

    2. Every File In Module Reside In Module Specific Package

    Enable Full Command Line Logging

    Boost command line output

    When running using non- the default command line output contains only the test name:

    To be able to see full exception log containing invalid declaration file path and line number enable exceptionFormat in Gradle testLogging:

    Now log output provides all informations relevant to pin point the invalid declaration:

    Changelog

    Stay up to date

    The full change log is available in the .

    Sponsor Konsist

    We appreciate your interest in Konsist. If you find our project valuable, please consider supporting Konsist through . Every contribution, no matter how small, adds up to make a significant impact.

    Personal Support

    Your personal sponsorship helps us continue our work and improve the tool for the entire community.

    @Test
    fun `property generic type does not contains star projection`() {
        Konsist
            .scopeFromProduction()
            .properties()
            .types
            .assertFalse { type ->
                type
                    .typeArguments
                    ?.flatten()
                    ?.any { it.isStarProjection }
            }
    }

    Konsist: First experience with the new linter for Kotlin

  • Refactoring Multi-Module Kotlin Project With Konsist

  • ArchUnit vs. Konsist. Why Did We Need Another Kotlin Linter?

  • Protect Kotlin Project Architecture Using Konsist

  • Konsist: Protect Kotlin Multiplatform projects from architecture guidelines violations

  • Konsist and Conquer: Embracing the World of Kotlin Dynamic Testing

  • Adding Konsist and Ktlint to a GitHub Actions Continuous Integration

  • Kotlin “Lint” Testing With Konsist

  • Konsist: Protect Kotlin Multiplatform projects from architecture guidelines violations

  • Konsist: First experience with the new linter for Kotlin

  • Stop Debating in Code Reviews. Start Enforcing with Lint Rules (Droidcon Berlin 2024)
    A Tour Through Konsist
    Standardisez votre codebase avec Konsist
    Introducing Konsist: A Cutting-Edge Kotlin Linter
    Stop Debating in Code Reviews. Start Enforcing with Lint Rules
    Konsist is more than you might think
    Konsist adoption with a custom Baseline definition
    Konsist repository
    Corporate Backing

    If your organization has open source funding initiatives, we'd be grateful if you could recommend Konsist or connect us with the appropriate team. Corporate backing can significantly boost our ability to enhance and sustain the project, benefiting the entire user community. For corporate sponsorship inquiries, please contact us at [email protected].

    OpenCollective
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>3.0.0</version>
        <configuration>
            <argLine>-Xmx1g</argLine>
        </configuration>
    </plugin>
    @Test
    fun `all generic return types contain Kotlin collection type argument`() {
        Konsist
            .scopeFromProduction()
            .functions()
            .returnTypes
            .withGeneric()
            .typeArguments
            .assertTrue { it.sourceDeclaration?.isKotlinCollectionType }
    }
    @Test
    fun `enum classes annotated with 'Serializable' have all enum constants annotated with 'SerialName'`() {
        Konsist.scopeFromProject()
            .classes()
            .withEnumModifier()
            .withAnnotationOf(Serializable::class)
            .enumConstants
            .assertTrue { it.hasAnnotationOf(SerialName::class) }
    }
    @Test
    fun `all models are serializable`() {
        Konsist
            .scopeFromPackage("com.myapp.model..")
            .classes()
            .assertTrue {
                it.hasAnnotationOf(Serializable::class)
            }
    }
    class UseCaseTest : FreeSpec({
        "UseCase has test class" {
            Konsist
                .scopeFromProject()
                .classes()
                .withNameEndingWith("UseCase")
                .assertTrue(testName = this.testCase.name.testName) { it.hasTestClasses() }
        }
    })
    class UseCaseTests : FreeSpec({
        Konsist
            .scopeFromProject()
            .classes()
            .withNameEndingWith("UseCase")
            .forEach { useCase ->
                "${useCase.name} should have test" {
                    useCase.assertTrue(testName = this.testCase.name.testName) { it.hasTestClasses() }
                }
                "${useCase.name} should reside in ..domain..usecase.. package" {
                    useCase.assertTrue(testName = this.testCase.name.testName) { it.resideInPackage("..domain..usecase..") }
                }
                "${useCase.name} should ..." {
                    // another Konsist assert
                }
            }
    })
    Kotest
    Dynamic Konsist Tests
    tasks.withType<Test> {
      testLogging {
        events(TestLogEvent.FAILED)
        exceptionFormat = TestExceptionFormat.FULL
      }
    }
    tasks.test { 
        testLogging { 
            events(TestLogEvent.FAILED)
            exceptionFormat = TestExceptionFormat.FULL 
        } 
    }
    For example, such declaration can be used to check if annotations reside in a desired package:

    Use Site

    Now consider this function:

    The above code also contains CustomLogger annotation. However, this time code represents the place in the code where the annotation is used (use-site). Such annotations can be accessed using the annotations property:

    Such properties can be used to check if the function annotated with CustomLogger annotation has the correct name prefix:

    annotation class CustomLogger
    koScope
        .classes()
        .withAnnotationModifier()
    // Every annotation class must reside in the "annotation" package
    
    koScope
        .classes()
        .withAnnotationModifier()
        .assertTrue { it.resideInPackage("..annotation..") }
    • The appropriate test names will appear in the log if the test fails.

    • Test suppression will be facilitated (See Suppress Konsist Test)

    See Explicit Test Names for more details.

    Kotest enables fetching the test name from the context to populate the testName argument, ensuring consistent naming of tests:

    This example is used FreeSpec however Kotest provides multiple testing styles.

    KoTestName Extension

    To facilitate test name retrieval you can add a custom koTestName extension:

    This extension enables more concise syntax for providing Kotest test name:

    The above test will execute multiple assertions per test (all use cases will be verified in a single test). If you prefer better isolation and more visibility you can execute every assertion as a separate test. See theDynamic Konsist Tests page.

    Kotest
    Kotest
    3. Every Function With Return Value Has A Return Tag

    4. Every Extension Has A Receiver Tag

    5. Every Public Function In Api Package Must Have Explicit Return Type

    6. Every Public Property In Api Package Must Have Specify Type Explicitly

    @Test
    fun `every api declaration has KDoc`() {
        Konsist
            .scopeFromPackage("..api..")
            .declarationsOf<KoKDocProvider>()
            .assertTrue { it.hasKDoc }
    }
    3. Test Classes Should Have Test Subject Named Sut

    4. Test Classes Should Have All Members Private Besides Tests

    @Test
    fun `every class has test`() {
        Konsist
            .scopeFromProduction()
            .classes()
            .assertTrue { it.hasTestClass() }
    }
    @Test
    fun `every class - except data and value class - has test`() {
        Konsist
            .scopeFromProduction()
            .classes()
            .withoutModifier(KoModifier.DATA, KoModifier.VALUE)
            .assertTrue { it.hasTestClass() }
    }

    3. Files Reside In Package That Is Derived From Module Name

    @Test
    fun `2 layer architecture has correct dependencies`() {
        Konsist
            .scopeFromProject()
            .assertArchitecture {
                val presentation = Layer("Presentation", "com.myapp.presentation..")
                val business = Layer("Business", "com.myapp.business..")
                val persistence = Layer("Persistence", "com.myapp.persistence..")
                val database = Layer("Database", "com.myapp.database..")
    
                presentation.dependsOn(business)
                business.dependsOn(presentation)
                business.dependsOn(persistence)
                persistence.dependsOn(business)
                business.dependsOn(database)
                database.dependsOn(business)
            }
    }
    @Test
    fun `every file in module reside in module specific package`() {
        Konsist
            .scopeFromProject()
            .files
            .assertTrue { it.packagee?.name?.startsWith(it.moduleName) }
    }
    @Test
    fun `function parameter has generic type argument with name ending with 'Repository'`() {
        Konsist
            .scopeFromProduction()
            .functions()
            .parameters
            .types
            .withGeneric()
            .sourceDeclarations()
            .assertFalse { it.hasNameEndingWith("Repository") }
    }
    // Code Snippet
    internal class Engine
    val current: Engine? = null
    
    // Konsist test
    Konsist
       .scopeFromProject()
       .properties()
       .assertTrue {
          it
          .type
          ?.sourceDeclaration
          ?.asClassDeclaration()
          ?.hasInternalModifier // true
       }
    
    // Code Snippet
    internal class Engine {
       fun start(): Boolean
    }
    
    // Konsist test
    Konsist
       .scopeFromProject()
       .classes()
       .functions()
       .assertTrue {
          it.returnType?
          .sourceDeclaration
          ?.isKotlinBasicType
       }
    // Code Snippet
    internal class Engine {
       fun start(): Boolean
    }
    
    // Konsist test
    Konsist
       .scopeFromProject()
       .classes()
       .parents()
       .assertTrue {
          it
          .sourceDeclaration
          ?.isInterface
       }
    > Task :konsistTest:test
    
    UseCaseKonsistTest > every use case has single public operator function named 'invoke' FAILED
        com.lemonappdev.konsist.core.exception.KoAssertionFailedException at UseCaseKonsistTest.kt:26
    
    2 tests completed, 1 failed
    
    > Task :konsistTest:testDebugUnitTest FAILED
    
    FAILURE: Build failed with an exception.
    
    
    > Task :konsistTest:test
    
    UseCaseKonsistTest > every use case has single public operator function named 'invoke' FAILED
        com.lemonappdev.konsist.core.exception.KoAssertionFailedException: Assert 'every use case has single public operator function named 'invoke'' was violated (25 times). Invalid declarations:
        /myproject/usecase/LoginUserUseCase.kt:6:1 (LoginUserUseCase ClassDeclaration)
        /myproject/usecase/GetLocationUseCase.kt:8:1 (GetLocationUseCase ClassDeclaration)
        
    2 tests completed, 1 failed
    
    > Task :konsistTest:testDebugUnitTest FAILED
    
    FAILURE: Build failed with an exception.
    
    @CustomLogger
    fun logHello() {
        println("Hello")
    }
    koScope
        .functions()
        .annotations
    // Every function with a name starting with "log" is annotated with CustomLogger
    
    koScope
        .functions()
        .withAllAnnotations("CustomLogger")
        .assertTrue {
            it.hasNameStartingWith("log")
        }
    class UseCaseTest : FreeSpec({
        "useCase test" {
            Konsist
                .scopeFromProject()
                .classes()
                .assertTrue (testName = this.testCase.name.testName) {  }
        }
    })
    val TestScope.koTestName: String
        get() = this.testCase.name.testName
    class UseCaseTest : FreeSpec({
        "useCase test" {
            Konsist
                .scopeFromProject()
                .classes()
                .assertTrue (testName = koTestName) {  } // extension used
        }
    })
    @Test
    fun `every function with parameters has a param tags`() {
        Konsist.scopeFromPackage("..api..")
            .functions()
            .assertTrue { it.hasValidKDocParamTags() }
    }
    @Test
    fun `every function with return value has a return tag`() {
        Konsist.scopeFromPackage("..api..")
            .functions()
            .assertTrue { it.hasValidKDocReturnTag() }
    }
    @Test
    fun `every extension has a receiver tag`() {
        Konsist.scopeFromPackage("..api..")
            .declarationsOf<KoReceiverTypeProvider>()
            .assertTrue { it.hasValidKDocReceiverTag() }
    }
    @Test
    fun `every public function in api package must have explicit return type`() {
        Konsist
            .scopeFromPackage("..api..")
            .functions()
            .assertTrue { it.hasReturnType() }
    }
    @Test
    fun `every public property in api package must have specify type explicitly`() {
        Konsist
            .scopeFromPackage("..api..")
            .properties()
            .assertTrue { it.hasType() }
    }
    @Test
    fun `test classes should have test subject named sut`() {
        Konsist
            .scopeFromTest()
            .classes()
            .assertTrue {
                val type = it.name.removeSuffix("Test")
                val sut = it
                    .properties()
                    .firstOrNull { property -> property.name == "sut" }
    
                sut != null && (sut.type?.name == type || sut.text.contains("$type("))
            }
    }
    @Test
    fun `test classes should have all members private besides tests`() {
        Konsist
            .scopeFromTest()
            .classes()
            .declarations()
            .filterIsInstance<KoAnnotationProvider>()
            .withoutAnnotationOf(Test::class, ParameterizedTest::class, RepeatedTest::class)
            .filterIsInstance<KoVisibilityModifierProvider>()
            .assertTrue { it.hasPrivateModifier }
    }
    @Test
    fun `files reside in package that is derived from module name`() {
        Konsist.scopeFromProduction()
            .files
            .assertTrue {
                /*
                module -> package name:
                feature_meal_planner -> mealplanner
                feature_caloric_calculator -> caloriccalculator
                */
                val featurePackageName = it
                    .moduleName
                    .removePrefix("feature_")
                    .replace("_", "")
    
                it.hasPackage("com.myapp.$featurePackageName..")
            }
    }
    ) and class declaration (
    KoClassDeclaration
    ). The
    Logger
    class declaration contains a single function declaration (
    KoFunctionDeclaration
    ):

    Declarations mimic the Kotlin file structure. Konsts API provides a way to retrieve every element. To get all functions in all classes inside the file using .classes().functions() :

    To print declaration content use koDeclaration.print() method.

    Declaration Properties

    Each declaration contains a set of properties to facilitate filtering and verification eg. KoClass declaration has name, modifiers , annotations , declarations (containing KoFunction) etc. Here is how the name of the function can be retrieved.

    Although it is possible to retrieve a property of a single declaration usually verification is performed on a collection of declarations matching certain criteria eg. methods annotated with specific annotations or classes residing within a single package. See the Declaration Filtering page.

    Debugging Declaration Properties

    Each declaration exposes a few additional properties to help with debugging:

    • text - provides declaration text eg. val property role = "Developer"

    • location - provides file path with file name, line, and column e.g. ~\Dev\IdeaProject\SampleApp\src\kotlin\com\sample\Logger:10:5

    • locationWithText - provides location together with the declaration text

    Konsist
    	.scopeFromProject()
    	.classes()
    	.first { it.name == "ClassC" }
    	.parents() // ClassB
    Konsist
    	.scopeFromProject()
    	.classes()
    	.first { it.name == "SampleClass" }
    	.parents(indirectParents = true) // ClassB, ClassA

    Add Konsist Existing To Project (Baseline)

    Retrofitting Konsist into a project that hasn't followed strict structural guidelines can pose initial challenges, necessitating a thoughtful approach to smoothly transition without disrupting ongoing development. Unlike most linters, which provide a baseline file, Konsist follows a different methodology (for now).

    The baseline file will be added in the future.

    There are two approaches that can be employed when retrofitting Konsist into an existing projectAdd Konsist Existing To Project (Baseline) and Suppress Annotation.

    Create Granular Scopes

    Scope represents a set of Kotlin files. The scope allows to verification of all Kotlin files in the project or only a subset of the project code base.

    See .

    When refactoring an existing application, you can either choose to first refactor a module and then add a Konsist test or initially add the Konsist test to identify errors, followed by the necessary refactor. Both strategies aim to ensure modules align with Konsist's structural guidelines.

    Consider this The MyDiet application with feature 3 modules:

    At first, the Konsist test can be applied to a single module:

    To review the content of a given scope see .

    As refactoring proceeds and code gets aligned, the Konsist scope can be extended to another feature module (featureGroceryListGenerator):

    When entire code base (all modules) are aligned with the Konsist tests, the scope can be retrieved from the entire project:

    Usage of project scope (scopeFromProject ) is a recommended approach because it helps to guard future modules without modifying the existing Konsist test.

    Konsist provides a flexible API to create scopes from modules, source sets, packages, files, etc., and combine these scopes together. See .

    Suppress Annotation

    The second approach, Suppress Annotation, may be helpful when to Konsist swiftly without making substantial alterations to the existing kotlin files. See .

    Compiler Type Inference

    The primary focus of Konsist API is to reflect the state of the Kotlin source code (that will be verified), not the state of the running program. The Kotlin compiler has a deeper understanding of the Kotlin code base than Konsist, because the compiler can infer more information. Let's take a look at a few examples.

    Class Example

    Consider this class:

    Is Logger class public? Yes obviously it is public, however, the hasPublicModifier method returns the false value:

    Why is that? The public visibility modifier is the default visibility modifier in Kotlin. Meaning that class will be public even if it does not have the explicit public modifier. Since the class has no public modifier the hasPublicModifier method returns false. To distinguish between class being public and class having explicitpublic modifier Konsist API provides another method to retrieve declaration visibility:

    Property Example

    Let's look at the name property:

    The name property is obviously of the String type. However String type is inferred, so Konsist has no way of Knowing the actual type (in this exact case this is achievable, but with more complex expressions containing delegates, setters, getters, or methods this approach would not work).

    The may enable this feature for Konsist.

    Primary Constructor Example

    Let's look at the primary constructor for the same class:

    The Logger the class has a primary constructor because the Kotlin compiler will generate a parameterless constructor under the hood. However, the Konsist API will return a null value because the primary constructor is not present in the Kotlin source code:

    Function Return Type Example

    Consider this function:

    Kotlin will infer String as the return type of the getName function. Since the source code does not contain this explicit return type Konsist lacks information about the return type. In this scenario, hasReturnType the method will return the false value:

    Unlike the previous example, Konsist has no way to determine the actual function return type.

    Konsist Snapshots

    Konsist occasionally releases snapshot versions to a dedicated snapshot repository. These snapshots provide early access to new features and bug fixes.

    Snapshot versions are development builds and may contain unstable features.

    Snapshot Release Process

    Currently, snapshots are released manually. At some point this process will be automated - new snapshot will be released each time code is merged to develop branch

    How to Use Snapshots

    Add Snapshot Repository

    First, you need to include the snapshot repository in your project configuration. Here's how to do it for different build systems:

    Add the following dependency to the module\pom.xml file:

    Add Konsist Dependency

    To use Konsist SNAPSHOT dependency changing version to X.Y.Z-SNAPSHOT (versions can be found in ):

    Add the following dependency to the module\build.gradle.kts file:

    Add the following dependency to the module\build.gradle file:

    Add the following dependency to the module\pom.xml file:

    JUnit Snippets

    Code snippets employed to ensure the uniformity of tests written with JUnit library.

    1. Classes With Test Annotation Should Have Test Suffix

    @Test
    fun `classes with 'Test' Annotation should have 'Test' suffix`() {
        Konsist
            .scopeFromSourceSet("test")
            .classes()
            .filter {
                it.functions().any { func -> func.hasAnnotationOf(Test::class) }
            }
            .assertTrue { it.hasNameEndingWith("Tests") }
    }

    2. Test Classes Should Have Test Subject Named Sut

    3. Test Classes Should Have All Members Private Besides Tests

    4. No Class Should Use JUnit4 Test Annotation

    Suppress Konsist Test

    The annotation serves as a powerful tool to control lines and static analysis tools. When writing the Konsist test, there might be instances where the specific guard is not applicable due to certain project-specific reasons. The @Suppress annotation can be used to ignore those particular issues, ensuring that the codebase still adheres to the overall linting standards

    In Konsist the @Suppress annotation parameter name is derived from the name of the test to be suppressed. For example - this test verifies if every API declaration has KDoc:

    The name of the test is every api declaration has KDoc, so we can suppress this test by using one of these arguments:

    Verify Interfaces

    Konsist enables development teams to enforce structural rules for interfaces ensuring code consistency across projects.

    To verify interfaces start by querying all interface present in the project:

    The above code selects all interfaces present in the project codebase. While this demonstrates Konsist's API capabilities, in practical scenarios you'll typically want to verify a specific subset of interface - such as those with a particular name suffix or interfaces within a given package. See and .

    Konsist allows you to verify multiple aspects of a interfaces. For a complete understanding of the available APIs, refer to the language reference documentation for .

    Let's look at few examples.

    Verify Functions

    Functions can be validated for their signatures, modifiers, naming patterns, return types, and parameter structures.

    To verify functions start by querying all functions present in the project:

    In practical scenarios you'll typically want to verify a specific subset of functions - such as those defined inside classes:

    Konsist API allows to query local functions:

    Konsist allows you to verify multiple aspects of a functions. For a complete understanding of the available APIs, refer to the language reference documentation for .

    Let's look at few examples.

    Android Snippets

    Konsist can be used to guard the consistency of the project.

    The project contains set of Konsist tests.

    1. Classes Extending ViewModel

    Verify Properties

    Properties can be checked for proper access modifiers, type declarations, and initialization patterns.

    To verify properties start by querying all properties present in the project:

    In practical scenarios you'll typically want to verify a specific subset of properties - such as those defined inside classes:

    Konsist allows you to verify multiple aspects of a properties. For a complete understanding of the available APIs, refer to the language reference documentation for KoPropertyDeclaration.

    Let's look at few examples.

    Project Status

    Where are we now?

    The Konsist linter has undergone extensive field testing across a variety of projects, including , , and , and is compatible with both and build systems. Additionally, Konsist features a comprehensive test suite with around 5,000 tests, running on various operating systems including MacOS, Windows, and Ubuntu, to minimize the risk of regressions.

    Konsist is safe for use because it is not bundled with the production code (only included in the test source sets).

    Konsist Roadmap

    Over the next few months, we will fix bugs, improve existing APIs, and implement missing features (in this order). Here is a high-level roadmap:

    Why There Are No Pre-defined Rules?

    Many linters including and have a predefined set of rules. These rules are derived and aligned with guidelines or common practices for writing high-quality code and industry coding conventions (, , etc.).

    However, there are no industry standards when comes to application architecture. Every code base is different - different class names, different package structures, different application layers, etc. As the project grows code base evolves as well - it tends to have more layers, more modules, and a more complex code structure. These "rules" are hard to capture by generic linter, because they are often specific to the given project.

    Let's consider a use case - a concept defined by the . At a high level the use case definition is quite simple - "use case holds a business logic". How the use case is represented in the code base? Well... In one project this may be a class that has a name ending with UseCase, in another, it may be a class extending BaseUseCase and in another class annotated with @UseCase annotation. The logic for filtering "all project use cases" will vary from project to project.

    private const val logLevel = "debug"
    
    @Entity
    open class Logger(val level: String) {
       fun log(message: String) {
       
       } 
    }
    koFile // List<KoFile>
        .classes()  // List<KoClassDeclaration>
        .functions() // List<KoFunctionDeclaration>
    val name = koFile // List<KoFileDeclaration>
        .classes()  // List<KoClassDeclaration>
        .functions() // List<KoFunctionDeclaration>
        .first() // KoFunctionDeclaration
        .name // String
        
    println(name) // prints: log
    class Logger

    ✅ Milestone 1 (Q1-Q2-Q3 2023)

    • ✅ Setup GitHub project

    • ✅ Setup CI pipeline

    • ✅ Core Library development

    • ✅ Publish artifact to Maven Central

    • ✅ Create documentation

    • ✅ Internal closed testing Android

    • ✅ Internal closed testing Spring

  • ✅ Milestone 2 (Q4 2023 Alpha)

    • ✅ Community-driven testing

    • ✅ Improve existing APIs

    • ✅ Fix Bugs

    • ✅ Polish documentation and samples

    • ✅ Implement new features

  • ✅ Milestone 3 (Q1 2024)

    • ✅ Stabilize APIs (minimal breaking changes)

    • ✅ Fix Bugs

    • ✅ Polish documentation and samples

    • ✅ Implement new features

  • 🚀 Milestone 4 (Q2 2024)

    • ✅ Declaration references

  • 🚀 Milestone 5 (Q3 2024)

    • 🚀 Architecture checks improvements

    • 🚀 Bug fixes

    • 🚀 API improvements

  • 🚀 Milestone 6 (Q4 2024 Beta)

    • 🚀 Bug fixes

    • 🚀 API improvements

    • 🚀 Release 1.0

  • 🕝 Milestone 5 (H1 2025)

    • 🕝 Further maintenance and improvements

  • Spring
    Android
    Kotlin Multiplatform
    Gradle
    Maven
    Now let's consider the actual structure of the use case class:
    • should every use case have UseCase a suffix in the class name?

    • can the use case be extended or include another use case?

    • should every use case reside in usecase the package?

    • should the use case have a single method?

    • how this method should be named?

    • can this method have overloads or should it be defined as invoke an operator?

    • should this method have a suspended modifier?

    • …

    Answers will vary from project to project. That is why Konsist favors a more flexible approach - it allows filtering members and defining custom code base assertions (tests). On top of that Konsist is utilizing Kotlin collection processing API to provide more control over filtering and asserting declarations (Declaration).

    Some things can be standardized across different projects e.g. constructor parameter names being derived from the property name, or alphabetic order of the parameter. For now, custom tests will be a core part of Konsist, however, we are considering the addition of a small set of predefined rules in the future.

    Detekt
    ktlint
    Kotlin coding conventions
    Android Kotlin style guide
    Clean Architecture
    Create The Scope
    Debug Konsist Test
    Create The Scope
    Add Konsist Existing To Project (Baseline)
    K2 Compiler plugin
    snapshot repository
    repositories {
        // Konsist snapshot repository
        maven("https://s01.oss.sonatype.org/content/repositories/snapshots/")
    
        // More repositorues
    }
    repositories {
        // Konsist snapshot repository
        maven {
            url 'https://s01.oss.sonatype.org/content/repositories/snapshots/'
        }
        
        // More repositories
    }
    <repositories>
        <!-- Konsist snapshot repository -->
        <repository>
            <id>konsist-snapshots</id>
            <url>https://s01.oss.sonatype.org/content/repositories/snapshots/</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </repository>
    
        <!-- More repositories -->
    </repositories>
    @Test
    fun `test classes should have test subject named sut`() {
        Konsist
            .scopeFromTest()
            .classes()
            .assertTrue {
                // Get type name from test class e.g. FooTest -> Foo
                val type = it.name.removeSuffix("Test")
                val sut = it
                    .properties()
                    .firstOrNull { property -> property.name == "sut" }
    
                sut != null && sut.hasTacitType(type)
            }
    }
    The name of the actual Konsist test -
    @Suppress("every api declaration has KDoc")
  • The name of the actual Konsist test was prefixed by konsist - @Suppress("konsist.every api declaration has KDoc"). This is helpful if you have multiple lines in the project and want to know which linters own a given check (check that needs to be suppressed)

  • In the below example, @Suppress annotation is applied to author property:

    Suppression without konsist.prefix also works (@Suppress("every api declaration has KDoc")). However, using the konsist.prefix is advised as it links the Suppress annotation to a specific tool. This ensures clarity on whether a particular suppression relates to a Konsist test.

    When using @Suppress annotation, it's advisable to apply it to the smallest possible scope to ensure that only the intended warnings are suppressed, so other potential issues aren't inadvertently overlooked. In the above example, the @Suppress annotation was applied to the property.

    If broader suppression is necessary, you can then escalate to the interface level:

    As a last resort, if multiple elements in a file need the same suppression, the @Suppress annotation can be applied to the entire file:

    Suppressing Kotest Test

    Konsist has no way of retrieving the name of the current Kotest test (unlike JUnit).

    See the Kotest Support page.

    To allow suppression (and correct test names) it is recommended to utilize the name derived from the Kotest context using the testName argument:

    To suppress such tests use the test name prefixed with konsist.:

    See KoTestName Extension to simplify the syntax for Kotest test name.

    @Suppress
    Verify Name

    Interface names can be validated to ensure they follow project naming conventions and patterns.

    Check if interface name ends with Repository:

    Verify Modifiers

    Interface modifiers can be validated to ensure proper encapsulation and access control.

    Check if interface has internal modifier:

    Verify Annotations

    Interface-level and member annotations can be verified for presence, correct usage, and required attribute values.

    Check if interface is annotated with Service annotation:

    Verify Package

    Package declarations can be validated to ensure classes are located in the correct package structure according to architectural guidelines.

    Check if interface has model package or sub-packages (.. means include sub-packages):

    Verify Methods

    Methods can be validated for their signatures, modifiers, annotations, naming patterns, return types, and parameter structures.

    Check if methods (functions defined inside interface) have name starting with Local:

    See .

    Verify Properties

    Properties can be checked for proper access modifiers, type declarations, and initialization patterns.

    Check if all properties (defined inside interface) has val modifiers:

    See Verify Properties.

    Verify Generic Type Parameters

    Generic type parameters and constraints can be checked for correct usage and bounds declarations.

    Check if interface has not type parameters:

    Verify Generic Type Arguments

    Generic type arguments can be checked for correct usage.

    Check if parent has no type arguments:

    Verify Parents

    Inheritance hierarchies, interfaces implementations, and superclass relationships can be validated.

    Check if interface extends CrudRepository:

    Verify Companion Objects

    Companion object declarations, their contents, and usage patterns can be verified for compliance.

    Check if interface has companion object:

    Verify Members Order

    The sequential arrangement of interface members can be enforced according to defined organizational rules.

    Check if interface properties are defined before functions:

    Create The Scope
    Declaration Filtering
    KoInterfaceDeclaration
    Verify Name

    Function names can be validated to ensure they follow project naming conventions and patterns.

    Check if function name starts with get :

    Verify Modifiers

    Function modifiers can be validated to ensure proper encapsulation and access control.

    Check if function has public or default (also public) modifier:

    Verify Annotations

    Function-level and member annotations can be verified for presence, correct usage, and required attribute values.

    Check if function is annotated with Binding annotation:

    Verify Body Type

    Functions with block bodies (using curly braces) can be validated to ensure compliance with code structure requirements:

    Expression body functions (using single-expression syntax) can be verified to maintain consistent style across the codebase:

    Verify Parameters

    Function parameters can be validated for their types, names, modifiers, and annotations to ensure consistent parameter usage.

    Check if function has parameter of type String:

    Verify Return Type

    Return types can be checked to ensure functions follow expected return type patterns and contracts.

    Check if function has Kotlin collection type:

    Verify Generic Parameters

    Generic type parameters can be validated to ensure proper generic type usage and constraints.

    Check if function has type parameters:

    Verify Generic Type Arguments

    Generic type arguments can be checked for correct usage.

    Check if return type has no type arguments:

    Verify Top Level

    Top-level functions (functions not declared inside a class) can be specifically queried and validated:

    This helps ensure top-level functions follow project conventions, such as limiting their usage or enforcing specific naming patterns.

    KoFunctionDeclaration
    Should Have
    ViewModel
    Suffix

    2. Every ViewModel Public Property Has Flow Type

    3. Repository Classes Should Reside In repository Package

    4. No Class Should Use Android Util Logging

    5. All JetPack Compose Previews Contain Preview In Method Name

    6. Every Class With Serializable Must Have Its Properties Serializable

    Android
    android-showcase
    Verify Name

    Property names can be validated to ensure they follow project naming conventions and patterns.

    Check if Boolean property has name starting with is:

    Verify Type

    Property types can be validated to ensure type safety and conventions:

    Verify Modifiers

    Property modifiers can be validated to ensure proper encapsulation:

    Verify Annotations

    Property annotations can be verified for presence and correct usage:

    Verify Accessors

    Getter and setter presence and implementation can be validated:

    Check if property has getter:

    Check if property has setter:

    Verify Initialization

    Property initialization can be verified:

    Verify Delegates

    Property delegates can be verified:

    Check if property has lazy delegate:

    Verify Visibility

    Property visibility scope can be validated:

    Check if property has internal modifier:

    Verify Mutability

    Property mutability can be checked.

    Check if property is immutable:

    Check if property is mutable:

    Konsist
        .scopeFromModule("featureCaloryCalculator")
        .classes()
        .assertTrue { it.hasTestClasses() }
    Konsist
        .scopeFromModule("featureCaloryCalculator", "featureGroceryListGenerator")
        .classes()
        .assertTrue { it.hasTest() }
    Konsist
        .scopeFromProject()
        .classes()
        .assertTrue { it.hasTest() }
    koClass.hasPublicModifier() // false
    koClass.isPublicOrDefault() // true
    private val name = String
    class Logger
    koClass.primaryConstructor // null
    fun getName() = "Konsist"
    koFunction.hasReturnType() // false
    dependencies {
        testImplementation("com.lemonappdev:konsist:X.Y.Z-SNAPSHOT")
    }
    dependencies {
        testImplementation "com.lemonappdev:konsist:X.Y.Z-SNAPSHOT"
    }
    <dependency>
        <groupId>com.lemonappdev</groupId>
        <artifactId>konsist</artifactId>
        <version>X.Y.Z-SNAPSHOT</version>
        <scope>test</scope>
    </dependency>
    @Test
    fun `test classes should have all members private besides tests`() {
        Konsist
            .scopeFromTest()
            .classes()
            .declarations()
            .filterIsInstance<KoAnnotationProvider>()
            .withoutAnnotationOf(Test::class, ParameterizedTest::class, RepeatedTest::class)
            .filterIsInstance<KoVisibilityModifierProvider>()
            .assertTrue { it.hasPrivateModifier }
    }
    @Test
    fun `no class should use JUnit4 Test annotation`() {
        Konsist
            .scopeFromProject()
            .classes()
            .functions()
            .assertFalse {
                it.annotations.any { annotation ->
                    annotation.fullyQualifiedName == "org.junit.Test"
                }
            }
    }
    fun `every api declaration has KDoc`() {
        // ...
    }
    package com.myapp.api
    
    /**
     * Represents a simple `Book` entity.
     */
    interface Book {
        
        /**
         * The title of the book.
         */
        val title: String
        
        @Suppress("konsist.every api declaration has KDoc")
        val author: String
    }
    package com.myapp.api
    
    /**
     * Represents a simple `Book` entity.
     */
    @Suppress("konsist.every api declaration has KDoc")
    interface Book {
        
        /**
         * The title of the book.
         */
        val title: String
        
        val author: String
    }
    @file:Suppress("konsist.every api declaration has KDoc")
    package com.myapp.api
    
    /**
     * Represents a simple `Book` entity.
     */
    interface Book {
        
        /**
         * The title of the book.
         */
        val title: String
        
        @Suppress("konsist.every api declaration has KDoc")
        val author: String
    }
    package com.api.test
    
    class UseCaseTest : FreeSpec({
        "useCase test" {
            Konsist
                .scopeFromProject()
                .classes()
                .assertTrue (testName = this.testCase.name.testName) {  }
        }
    })
    package com.api.controller
    
    @Suppress("konsist.useCase test")
    class MyUseCase {
        ... // code
    }
    Konsist
    .scopeFromProject()
    .interfaces()
    ...
    ..
    .assertTrue {
       it.hasNameEndingWith("Repository")
    }
    ..
    .assertTrue {
       it.hasInternalModifier
    }
    ...
    .assertTrue {
       it.hasAnnotationOf(Service::class)
    }
    ...
    .assertTrue {
       it.resideInPackage("com.lemonappdev.model..")
    }
    ...
    .functions()
    .assertTrue {
        it.hasNameStartingWith("Local")
    }
    ...
    .properties()
    .assertTrue {
       it.isVal
    }
    ...
    .assertFalse {
        it.hasTypeParameters()
    }
    ...
    .parents()
    .assertFalse {
        it.hasTypeArguments()
    }
    ...
    .assertTrue {
       it.hasParentOf(CrudRepository::class)
    }
    ...
    .assertTrue {
       it.hasObject { objectt -> objectt.hasCompanionModifier }
    }
    ...
    .assertTrue {
        val lastKoPropertyDeclarationIndex = it
            .declarations(includeNested = false, includeLocal = false)
            .indexOfLastInstance<KoPropertyDeclaration>()
        
        val firstKoFunctionDeclarationIndex = it
            .declarations(includeNested = false, includeLocal = false)
            .indexOfFirstInstance<KoFunctionDeclaration>()
        
        if (lastKoPropertyDeclarationIndex != -1 && firstKoFunctionDeclarationIndex != -1) {
            lastKoPropertyDeclarationIndex < firstKoFunctionDeclarationIndex
        } else {
            true
        }
    }
    Konsist
    .scopeFromProject()
    .functions()
    ...
    Konsist
    .scopeFromProject()
    .classes()
    .functions()
    ...
    Konsist
    .scopeFromProject()
    .classes()
    .functions(includeLocal = true)
    ...
    ...
    .assertTrue {
       it.hasNameStartingWith("get")
    }
    ..
    .assertTrue {
       it.hasPublicOrDefaultModifier
    }
    ...
    .assertTrue {
       it.hasAnnotationOf(Binding::class)
    }
    ...
    .assertTrue { 
        it.hasBlockBody 
    }
    ...
    .assertTrue { 
        it.hasExpressionBody 
    }
    ...
    .assertTrue { 
        it.hasParameter { parameter  -> parameter.hasTypeOf(String::class) }
    }
    ...
    .assertTrue { 
        it.returnType?.sourceDeclaration?.isKotlinCollectionType
    }
    ...
    .assertTrue { 
        it.hasTypeParameters()
    }
    ...
    .assertFalse {
        it.returnType?.hasTypeArguments()
    }
    ...
    .assertTrue { 
        it.isTopLevel
    }
    @Test
    fun `classes extending 'ViewModel' should have 'ViewModel' suffix`() {
        Konsist
            .scopeFromProject()
            .classes()
            .withParentClassOf(ViewModel::class)
            .assertTrue { it.name.endsWith("ViewModel") }
    }
    @Test
    fun `Every 'ViewModel' public property has 'Flow' type`() {
        Konsist
            .scopeFromProject()
            .classes()
            .withParentClassOf(ViewModel::class)
            .properties()
            .assertTrue {
                it.hasPublicOrDefaultModifier && it.hasType { type -> type.name == "kotlinx.coroutines.flow.Flow" }
            }
    }
    @Test
    fun `'Repository' classes should reside in 'repository' package`() {
        Konsist
            .scopeFromProject()
            .classes()
            .withNameEndingWith("Repository")
            .assertTrue { it.resideInPackage("..repository..") }
    }
    @Test
    fun `no class should use Android util logging`() {
        Konsist
            .scopeFromProject()
            .files
            .assertFalse { it.hasImport { import -> import.name == "android.util.Log" } }
    }
    @Test
    fun `All JetPack Compose previews contain 'Preview' in method name`() {
        Konsist
            .scopeFromProject()
            .functions()
            .withAnnotationOf(Preview::class)
            .assertTrue {
                it.hasNameContaining("Preview")
            }
    }
    @Test
    fun `every class with Serializable must have its properties Serializable`() {
        val message =
            """In Android, every serializable class must implement the Serializable interface 
        |or be a simple non-enum type because this is how the Java and Android serialization 
        |mechanisms identify which objects can be safely converted to a byte stream for 
        |storage or transmission, ensuring that complex objects can be properly reconstructed 
        |when deserialized.""".trimMargin()
    
        Konsist
            .scopeFromProduction()
            .classes()
            .withParentNamed("Serializable")
            .properties()
            .types
            .sourceDeclarations()
            .withoutKotlinBasicTypeDeclaration()
            .withoutClassDeclaration { it.hasEnumModifier }
            .assertTrue(additionalMessage = message) {
                it.asClassDeclaration()?.hasParentWithName("Serializable")
            }
    }
    Konsist
    .scopeFromProject()
    .properties()
    ...
    Konsist
    .scopeFromProject()
    .classes()
    .properties()
    ...
    ...
    .assertTrue { 
        it.type?.name == "Boolean" && it.hasNameStartingWith("is")
    }
    ...
    .assertTrue { 
        it.type?.name == "LocalDateTime"
    }
    ...
    .assertTrue { 
        it.hasLateinitModifier
    }
    ...
    .assertTrue { 
        it.hasAnnotationOf(JsonProperty::class)
    }
    ...
    .assertTrue { 
        it.hasGetter
    }
    ...
    .assertTrue { 
        it.hasSetter
    }
    ...
    .assertTrue { 
        it.isInitialized
    }
    ...
    .assertTrue { 
        it.hasDelegate("lazy") 
    }
    ...
    .assertTrue { 
        it.isInternal
    }
    ...
    .assertTrue { 
        it.isVal
    }
    ...
    .assertTrue { 
        it.isVar
    }

    Declaration Assertion

    Verify codebase using Konsist API

    Assertions are used to perform code base verification. This is the final step of Konsist verification preceded by scope creation (Create The Scope) and Declaration Filtering steps:

    Assertion Methods

    Konsist offers a variety of assertion methods. These can be applied to a list of KoDeclarations as well as a single declaration.

    Assert True

    In the below snippet, the assertion (performed on the list of interfaces) verifies if every interface has a public visibility modifier.

    The it parameter inside the assertTrue method represents a single declaration (single interface in this case). However, the assertion itself will be performed on every available interface. The last line in the assertTrue block will be evaluated as true or false providing the result for a given asset.

    Each KoDeclaration comes with an API, comprising methods and properties, for verifying the declaration. Additionally, the Konsist API offers a text property for exceptional cases where the standard API falls short. This should be used as a last resort, and any issues encountered should be reported .

    Assert False

    The assertFalse is a negation of the assertTrue method. In the below snippet, the assertion (performed on the list of properties) verifies if none of the properties has the Inject annotation:

    This assertion verifies that the class does not contain any properties with public (an explicit public modifier) or default (implicit public modifier) modifiers:

    Assert Empty

    This assertion helps to verify if the given list of declarations is empty.

    Assert Not Empty

    This assertion helps to verify if the given list of declarations is not empty.

    Assertion Parameters

    Test Name

    Assertions offer a set of parameters allowing to tweak the assertion behavior. You can adjust several settings, such as setting testName that helps with suppression (see ).

    Strict

    You can also enable enhanced verification by setting strict argument to true:

    Additional Message

    The additionalMessage param allows to provision of additional messages that will be displayed with the failing test. This may be a more detailed description of the problem or a hint on how to fix the issue.

    Verify Classes

    Konsist enables development teams to enforce structural rules for class ensuring code consistency across projects.

    To verify classes start by querying all classes present in the project:

    The above code selects all classes present in the project codebase. While this demonstrates Konsist's API capabilities, in practical scenarios you'll typically want to verify a specific subset of classes - such as those with a particular name suffix or classes within a given package. See Create The Scope and Declaration Filtering.

    Konsist allows you to verify multiple aspects of a class. For a complete understanding of the available APIs, refer to the language reference documentation for KoClassDeclaration.

    Let's look at few examples.

    Verify Name

    Class names can be validated to ensure they follow project naming conventions and patterns.

    Check if class name ends with Repository:

    Verify Modifiers

    Class modifiers can be validated to ensure proper encapsulation and access control.

    Check if class has internal modifier:

    Verify Annotations

    Class-level and member annotations can be verified for presence, correct usage, and required attribute values.

    Check if class is annotated with Service annotation:

    Verify Package

    Package declarations can be validated to ensure classes are located in the correct package structure according to architectural guidelines.

    Check if class has model package or sub-packages (.. means include sub-packages):

    Verify Methods

    Methods can be validated for their signatures, modifiers, annotations, naming patterns, return types, and parameter structures.

    Check if methods (functions defined inside class) have no annotations:

    See .

    Verify Properties

    Properties can be checked for proper access modifiers, type declarations, and initialization patterns.

    Check if all properties (defined inside class) has val modifiers:

    See .

    Verify Constructors

    Primary and secondary constructors can be validated for parameter count, types, and proper initialization.

    Check if class has explicit primary constructor:

    Check if primary constructor is annotated with Inject annotation:

    Verify Generic Type Parameters

    Generic type parameters and constraints can be checked for correct usage and bounds declarations.

    Check if class has not type parameters:

    Verify Generic Type Arguments

    Generic type arguments can be checked for correct usage.

    Check if parent has no type arguments:

    Verify Parents

    Inheritance hierarchies, interfaces implementations, and superclass relationships can be validated.

    Check if class extends CrudRepository:

    Verify Companion Objects

    Companion object declarations, their contents, and usage patterns can be verified for compliance.

    Check if class has companion object:

    Verify Members Order

    The sequential arrangement of class members can be enforced according to defined organizational rules.

    Check if class properties are defined before functions:

    Verify Generics

    Type parameter vs type argument

    To undersigned Konsist API let's look at the difference between generic type parameters and generic type arguments:

    1. Type Parameter is the placeholder (like T) you write when creating a class or function (declaration site)

    2. Type Argument is the actual type (like String or Int) you provide when using that class or function (use site)

    Simple Examples:

    Verify Type Parameters

    Type parameters can be defined, for example, inside class or function.

    Check whether a class's generic type parameter has the name UiState:

    Check whether function type parameters has out modifier:

    Verify Type Arguments

    Check whether a property generic type argument has the name Service:

    The flatten() extension method allows to flatten type parameters structure:

    • For a type argument like String, it returns listOf().

    • For a type argument like List<String>, it returns listOf(String).

    • For a type argument like Map<List<String>, Int>, it returns

    Check if all functions parameters are have generic type argument ending with UIState:

    Check all parents have `String` type argument:

    Starter Projects

    Konsist provides preconfigured sample projects. Each project contains a complete build script config and a simple Konsist test. Projects are available in the starter-projects directory. Each JUnit5 and Kotest project has an additional dynamic test (Dynamic Konsist Tests)(dynamic tests are currently available at the develop branch).

    JUnit 4
    JUnit 5
    Kotest

    Android

    Static

    Static + Dynamic

    Static + Dynamic

    Projects:

    Android

    • JUnit 4

    • JUnit 5

    Spring

    • JUnit 5

    KMP

    • JUnit5

    • Kotest

    Debug Konsist Test

    Understand whats going on

    To gain insight into the inner workings of the Konsist test, examine the data provided by the Konsist API.

    Two primary tools can help you comprehend the inner workings of the Konsist API are Debug Konsist Test and Print To Console.

    Evaluate Expression Debugger Window

    The IntelliJ IDEA / Android Studio provides a handy feature called Evaluate Expressions which is an excellent tool for debugging Konsist tests.

    Create a simple test class and click on the line number to add the :

    Debug the test:

    When the program stops at the breakpoint (blue line background) run Evaluate Expression... action...

    ...or press Evaluate Expression... button:

    In the Evaluate window enter the code and click the Evaluate the button. For example, you can list all of the classes present in the scope to get the class names:

    You can also display a single-class declaration to view its name:

    Print To Console

    Konsist provides a flexible API that allows to output of the specified data as console logs. Scopes, lists of declarations, and single declarations can all be printed.

    Print a list of files from KoScope:

    Print multiple declarations:

    Print a given attribute for each declaration:

    Print single declaration:

    Print list of queried declarations before and after query:

    Print nested declarations:

    Open Source Licenses

    Konsist

    The Konsist project is licensed under Apache License-2.0.

    Dependencies

    Third-party libraries, plugins, and tools that Konsist project uses:

    Name
    Licence
    Page

    See file for more details.

    Create Secound Konsist Test - Architectural Check

    Konsist's Architectural Checks serve as a robust tool for maintaining layer isolation, enabling development teams to enforce strict boundaries between different architectural layers. Here few things that can be verified with Konsist:

    • domain layer is independant

    • data layer depends on domain layer

    Clean Architecture Snippets

    Snippets used to guard clean architecture dependencies.

    1. Clean Architecture Layers Have Correct Dependencies

    Konsist
    .scopeFromProject()
    .classes()
    ...
    • android-gradle-groovy-junit-5

    • android-gradle-kotlin-junit-5

  • Kotest

    • android-gradle-groovy-kotest

    • android-gradle-kotlin-kotest

  • Kotest

    • spring-gradle-kotlin-kotest

    • spring-gradle-groovy-kotest

    • spring-maven-kotest

  • Spring

    Static

    Static + Dynamic

    Static + Dynamic

    Kotlin Multiplatform

    Static

    Static + Dynamic

    Static + Dynamic

    android-gradle-groovy-junit-4
    android-gradle-kotlin-junit-4
    spring-gradle-groovy-junit-5
    spring-gradle-kotlin-junit-5
    konsist-starter-kmp-gradle-kotlin-junit5
    konsist-starter-kmp-gradle-kotlin-kotest
    spring-maven-junit5
    Verify Properties
    listOf("List, String, Int)
    .
    breakpoint
    2. Classes With
    UseCase
    Suffix Should Reside In
    domain
    And
    usecase
    Package

    3. Classes With UseCase Suffix Should Have Single public Operator Method Named invoke

    4. Classes With UseCase Suffix And Parents Should Have Single public Operator Method Named invoke

    5. Interfaces With Repository Annotation Should Reside In data Package

    6. Every UseCase Class Has Test

    ...
    .assertTrue {
       it.hasNameEndingWith("Repository")
    }
    ...
    .assertTrue {
       it.hasInternalModifier
    }
    ...
    .assertTrue {
       it.hasAnnotationOf(Service::class)
    }
    ...
    .assertTrue {
       it.resideInPackage("com.lemonappdev.model..")
    }
    ...
    .functions()
    .assertTrue {
       it.annotations.isEmpty()
    }
    ...
    .properties()
    .assertTrue {
       it.isVal
    }
    ...
    .assertTrue {
       it.hasPrimaryConstructor
    }
    ...
    .primaryConstructors
    .assertTrue {
        it.hasAnnotation(Inject::class)
    }
    ...
    .assertFalse {
        it.hasTypeParameters()
    }
    ...
    .parents()
    .assertFalse {
        it.hasTypeArguments()
    }
    ...
    .assertTrue {
       it.hasParentOf(CrudRepository::class)
    }
    ...
    .assertTrue { declaration ->
        declaration.hasObject { it.hasCompanionModifier }
    }
    ...
    .assertTrue {
        val lastKoPropertyDeclarationIndex = it
            .declarations(includeNested = false, includeLocal = false)
            .indexOfLastInstance<KoPropertyDeclaration>()
        
        val firstKoFunctionDeclarationIndex = it
            .declarations(includeNested = false, includeLocal = false)
            .indexOfFirstInstance<KoFunctionDeclaration>()
        
        if (lastKoPropertyDeclarationIndex != -1 && firstKoFunctionDeclarationIndex != -1) {
            lastKoPropertyDeclarationIndex < firstKoFunctionDeclarationIndex
        } else {
            true
        }
    }
    // Example 1: Class
    // Here 'T' is a TYPE PARAMETER
    class Box<T>(val item: T)
    
    // Here 'String' is a TYPE ARGUMENT
    val stringBox = Box<String>("Hello")
    
    
    // Example 2: Function
    // Here 'T' is a TYPE PARAMETER
    fun <T> printWithType(item: T) {
        println("Type is: ${item::class.simpleName}")
    }
    
    // Here 'String' and 'Int' are TYPE ARGUMENTS
    printWithType<String>("Hello")  // prints: Type is: String
    // Code Snippet 
    class View<UiState>(val state: UiState) // UiState is typeParamener 
    
    // Konsist
    Konsist
        .scopeFromProject()
        .classes()
        .typeParameters // access type parameters
        .assertTrue {
            it.name == "UiState" // true
        }
    //Code Snippet 
    fun <out T> setState(item: T?) {
        // ...
    }
    
    // Konsist
    Konsist
        .scopeFromProject()
        .functions()
        .typeParameters // access type parameters
        .assertTrue {
            it.hasOutModifier // true
        }
    //Code Snippet 
    val services: List<Service> = emptyList()
    
    // Konsist
    Konsist
        .scopeFromProject()
        .properties()
        .assertTrue { property ->
            property
                .type
                ?.typeArguments
                ?.flatten()
                ?.any { typeArgument -> typeArgument.name == "Service" }
        }
    // Snippet 
    fun setState(uiState: View<WelcomeUIState>)
    
    // Konsist Test
    Konsist
        .scopeFromProject()
        .properties()
        .parameters
        .types
        .typeArguments
        .assertTrue { 
            it.hasNameEndingWith("UIState")  // true
        }
    // Snippet 
    open class Container<T>(private val item: T) { }
    class StringContainer(text: String) : Container<String>(text) { }
    
    // Konsist Test
    Konsist
        .scopeFromProject()
        .classes()
        .parents()
        .typeArguments
        .flatten()
        .assertTrue { 
            it.name == "String"  // true
        }
    koScope
        .classes()
        .first()
        .name
    koScope // KoScope
        .print()
    koScope
        .classes() // List<KoClassDeclaration>
        .print()
    koScope
        .classes() // List<KoClassDeclaration>
        .print { it.fullyQualifiedName }
    koScope
        .classes() // List<KoClassDeclaration>
        .first() // KoClassDeclaration
        .print()
    koScope
        .classes() // List<KoClassDeclaration>
        .print(prefix = "Before") // or .print(prefix = "Before") { it.name }
        .withSomeAnnotations("Logger")
        .print(prefix = "After") // or .print(prefix = "After") { it.name }
    koScope
        .classes() // List<KoClassDeclaration>
        .constructors // List<KoConstructorDeclaration>
        .parameters //  List<KoParameterDeclaration>
        .print()
    @Test
    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)
            }
    }
    @Test
    fun `classes with 'UseCase' suffix should reside in 'domain' and 'usecase' package`() {
        Konsist
            .scopeFromProject()
            .classes()
            .withNameEndingWith("UseCase")
            .assertTrue { it.resideInPackage("..domain..usecase..") }
    }
    @Test
    fun `classes with 'UseCase' suffix should have single 'public operator' method named 'invoke'`() {
        Konsist
            .scopeFromProject()
            .classes()
            .withNameEndingWith("UseCase")
            .assertTrue {
                val hasSingleInvokeOperatorMethod = it.hasFunction { function ->
                    function.name == "invoke" && function.hasPublicOrDefaultModifier && function.hasOperatorModifier
                }
    
                hasSingleInvokeOperatorMethod && it.countFunctions { item -> item.hasPublicOrDefaultModifier } == 1
            }
    }
    @Test
    fun `classes with 'UseCase' suffix and parents should have single 'public operator' method named 'invoke'`() {
        Konsist
            .scopeFromProject()
            .classes()
            .withNameEndingWith("UseCase")
            .assertTrue {
                // Class and it's parent
                val declarations = listOf(it) + it.parents(true)
    
                // Functions from all parents without overrides
                val uniqueFunctions = declarations
                    .mapNotNull { koParentDeclaration -> koParentDeclaration as? KoFunctionProvider }
                    .flatMap { koFunctionProvider ->
                        koFunctionProvider.functions(
                            includeNested = false,
                            includeLocal = false
                        )
                    }
                    .filterNot { koFunctionDeclaration -> koFunctionDeclaration.hasOverrideModifier }
    
                val hasInvokeOperatorMethod = uniqueFunctions.any { functionDeclaration ->
                    functionDeclaration.name == "invoke" && functionDeclaration.hasPublicOrDefaultModifier && functionDeclaration.hasOperatorModifier
                }
    
                val numParentPublicFunctions = uniqueFunctions.count { functionDeclaration ->
                    functionDeclaration.hasPublicOrDefaultModifier
                }
    
                hasInvokeOperatorMethod && numParentPublicFunctions == 1
            }
    }
    @Test
    fun `interfaces with 'Repository' annotation should reside in 'data' package`() {
        Konsist
            .scopeFromProject()
            .interfaces()
            .withAnnotationOf(Repository::class)
            .assertTrue { it.resideInPackage("..data..") }
    }
    @Test
    fun `every UseCase class has test`() {
        Konsist
            .scopeFromProduction()
            .classes()
            .withNameEndingWith("UseCase")
            .assertTrue { it.hasTestClasses() }
    }

    Gradle Test Logger Plugin

    Apache-2.0 License

    JUnit

    Eclipse Public License - v 2.0

    Kotest

    Apache-2.0 License

    Gradle

    Apache-2.0 License

    Ktlint

    MIT License

    Detekt

    Apache-2.0 License

    Dokka

    Apache-2.0 License

    Kotlin

    Apache-2.0 License

    https://github.com/JetBrains/kotlin

    Kotlin-compiler

    Apache-2.0 License

    https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-compiler

    Mockk

    Apache-2.0 License

    https://mockk.io/

    Spotless

    libs.versions.toml

    Apache-2.0 License

    ...

    See Architecture Snippetssection for more examples.

    Write First Architectural Check

    Let's write a simple test to verify that application architecture rules are preserved. In this scenario, the application follows a simple 3-layer architecture, where Presentation and Data layers depend on Domain layer and Domain layer is independant (from these layers):

    Overview

    On a high level writing Konsist architectural check requires 3 steps:

    Let's take a closer look at each of these steps.

    1. Define Layers

    Create layers instances to represent project layers. Each Layer instance accepts the name (used for presenting architecture violation errors) and package used to define layers.

    The double dot syntax (..) means zero or more packages - layer is represented by the package and all of it's sub packages (seePackage Wildcard syntax).

    2. Create The Scope

    The Konsist object is an entry point to the Konsist library.

    The scopeFromX methods obtains the instance of the scope containing Kotlin project files. To get all Kotlin project files present in the project use the scopeFromProject method:

    To define more granular scopes such as scope from production code or scope from single module see the Create The Scope page.

    3. Assert Architecture

    To performa assertion use the assertArchiteture method:

    Utilize dependsX methods to validate that your project's layers adhere to the defined architectural dependencies:

    Wrap Konsist Code In Test

    The declaration validation logic should be protected through automated testing. By wrapping Konsist checks within standard testing frameworks such as JUnit or KoTest, you can verify these rules with each Pull Request:

    The JUnit testing framework project dependency should be added to the project. See starter projects to get a complete sample project.

    For Kotest to function correctly the Kotest test name has to be explicitly passed. See theKotest Support page.

    The testing framework project dependency should be added to the project. See to get a complete sample project.

    Note that test class has a KonsistTest suffix. This is the recommended approach to name classes containing Konsist tests.

    Summary

    This section described the basic way of writing Konsist architectural test. To get a better understanding of how Konsist API works see Debug Konsist Test.

    Getting Help
    Suppress Konsist Test

    Create First Konsist Test - Declaration Check

    Konsist Declaration Checks provide a powerful mechanism for validating the structural elements of the Kotlin codebase. These checks allow developers to enforce structural rules and coding conventions by verifying classes, interfaces, functions, properties, and other code declarations. Here few things that can be verified with Konsist:

    • All Use cases should reside in usecase specific package

    • Repository classes must implement Repository interface

    • All repository classes should have name ending with Repository

    • data classes should have only val properties

    • Test classes should have test subject named sut

    • ...

    See section for more examples.

    Write First Declaration Check

    Let's write a simple test to verify that all classes (all class declarations) residing in resides in controller package are annotated with the RestController annotation .

    Overview

    On a high level writing Konsist declaration check requires 4 steps:

    Let's take a closer look at each of these steps.

    1. Create The Scope

    The first step is to get a list of Kotlin files to be verified.

    The Konsist object is an entry point to the Konsist library.

    The scopeFromX methods obtains the instance of the scope containing Kotlin project files. To get all Kotlin project files present in the project use the scopeFromProject method:

    To define more granular scopes such as scope from production code or scope from single module see the page.

    2. Retrieve Declarations

    Each file in the scope contains set of declarations like classes, properties functions etc. (see ). To write this declaration check for all classes present in the scope query classes using classes method :

    3. Filter Declarations

    In this project controllers are defined as classes annotated with RestController annotation. Use withAllAnnotationsOf method to filter classes with with RestController annotation:

    To perform more granular querying and filtering see the page.

    4. Define Assertion

    To performa assertion use the assertTrue method:

    To verify that classes are located in the controller package, use the resideInPackage method inside assertTrue block:

    This verification applies to the entire collection of previously filtered classes, rather than examining just one class in isolation.

    To learn more about assertions see page.

    The double dot syntax (..) means zero or more packages - controller package preceded by any number of packages (see syntax).

    Wrap Konsist Code In Test

    The declaration validation logic should be protected through automated testing. By wrapping Konsist checks within standard testing frameworks such as or , you can verify these rules with each :

    The testing framework project dependency should be added to the project. See to get a complete sample project.

    For Kotest to function correctly the Kotest test name has to be explicitly passed. See the page.

    Note that test class has a KonsistTest suffix. This is the recommended approach to name classes containing Konsist tests.

    Summary

    This section described the basic way of writing Konsist declaration test. To get a better understanding of how Konsist API works see and sections.

    The above test will execute multiple assertions per test (all controllers will be verified in a single test). If you prefer better isolation each assertion can be executed as a separate test. See the page.

    Explicit Test Names

    For dynamic tests, Konsist can't obtain the current test's name. Test name may be correctly displayed in the IDE, however, the testName argument should be provided to enable:

    • Correct test names are displayed in the log when the test is failing

    • Test suppression (See Suppress Konsist Test)

    See .

    The testName argument should be passed to assertX methods such as assertTrue , assertFalse etc. Let's look at the code:

    Here is the summary of test frameworks:

    Testing Framework
    Determination
    Pass testName?

    Here is a concrete implementation passing he testName argument for each test Framework:

    introduced native support for dynamic tests, however, it also supports static tests. For static test testName does not have to be passed as it can be internally retrieved by Konsist.

    introduced native support for dynamic tests, allowing tests to be generated at runtime through the @TestFactory annotation.

    provides robust support for dynamic tests, allowing developers to define test cases programmatically at runtime, making it a flexible alternative to traditional JUnit testing. It is recommended to utilize the name derived from the Kotest (this.testCase.name.testName) context as the value for the testName argument:

    Architecture Assertion

    Verify codebase using Konsist API

    Architecture assertions are used to perform architecture verification. It is the final step of Konsist verification preceded by scope creation (Create The Scope):

    Assert Architecture

    As an example, this simple 2-layer architecture will be used:

    The assertArchitecture block defines architecture layer rules and verifies that the layer requirements are met.

    Define Layers

    Create class instance to represent project layers. Each Layer instance accepts the name (used for presenting architecture violation errors) and package used to define architectural layer:

    The inclusion of two trailing dots indicates that the layer is denoted by the com.myapp.business package together with all of its sub-packages.

    Define Architecture Assertions

    The final step is to define the dependencies (relations) between each layer using one of these methods:

    • dependsOn

    • dependsOnNothing

    • doesNotDependOn

    See the for above methods.

    The above methods follow up the layer definitions inside assertArchitecture block:

    Strict DependsOn

    By default dependsOn method works like does not perform strict layer validation (strict = false). However this behaviour is controlled b ystrict parameter:

    • strict = false (default) - may depend on layer

    • strict = true - have to depend on layer

    e.g.

    Excluding Files

    Architecture verification can be performed on KoScope (as seen above) and a list containing KoFiles. For example, you can remove a few files from the scope before performing an architectural check:

    This approach provides more flexibility when working with complex projects, however, The desired approach is to create a dedicated scope. See .

    Include Layer Without Defining Dependency

    The method allows to include layer in architecture verification, without defining a dependency for this layer:

    Architecture As A Variable

    Architecture configuration can be defined beforehand and stored in a variable to facilitate checks for multiple scopes:

    This approach may be helpful when refactoring existing applications. To facilitate readability the above checks should be expressed as two unit tests:

    class ArchitectureKonsistTest {
        @Test
        fun `architecture layers have dependencies correct`() {
            Konsist
                .scopeFromProject()
                .assertArchitecture {
                    private val presentationLayer = Layer("Presentation", "com.myapp.presentation..")
                    private val domainLayer = Layer("Domain", "com.myapp.business..")
                    private val dataLayer = Layer("Data", "com.myapp.data..")
            
                    // Define layer dependnecies
                    presentationLayer.dependsOn(domainLayer)
                    dataLayer.dependsOn(domainLayer)
                    domainLayer.dependsOnNothing()
                }
        }
    }
    class ArchitectureKonsistTest {
        class UseCaseTest : FreeSpec({
            "architecture layers have dependencies correct" {
                Konsist
                    .scopeFromProject()
                    .assertArchitecture {
                        private val presentationLayer = Layer("Presentation", "com.myapp.presentation..")
                        private val domainLayer = Layer("Domain", "com.myapp.business..")
                        private val dataLayer = Layer("Data", "com.myapp.data..")
                
                        // Define layer dependnecies
                        presentationLayer.dependsOn(domainLayer)
                        dataLayer.dependsOn(domainLayer)
                        domainLayer.dependsOnNothing()
                    }
            }
        })
    }
    // Define layers
    private val presentationLayer = Layer("Presentation", "com.myapp.presentation..")
    private val domainLayer = Layer("Domain", "com.myapp.domain..")
    private val dataLayer = Layer("Data", "com.myapp.data..")
    Konsist
    // Define layers
    private val presentationLayer = Layer("Presentation", "com.myapp.presentation..")
    private val domainLayer = Layer("Domain", "com.myapp.domain..")
    private val dataLayer = Layer("Data", "com.myapp.data..")
     
    // Define the scope containing all Kotlin files present in the project
    Konsist.scopeFromProject() //Returns KoScope
    // Define layers
    private val presentationLayer = Layer("Presentation", "com.myapp.presentation..")
    private val domainLayer = Layer("Domain", "com.myapp.domain..")
    private val dataLayer = Layer("Data", "com.myapp.data..")
    
    Konsist
        .scopeFromProject()
         // Assert architecture
        .assertArchitecture {
            // Define architectural rules
        }
    Konsist
        .scopeFromProject()
        .assertArchitecture {
            private val presentationLayer = Layer("Presentation", "com.myapp.presentation..")
            private val domainLayer = Layer("Domain", "com.myapp.business..")
            private val dataLayer = Layer("Data", "com.myapp.data..")
    
            // Define layer dependnecies
            presentationLayer.dependsOn(domainLayer)
            dataLayer.dependsOn(domainLayer)
            domainLayer.dependsOnNothing()
        }
    koScope
        .interfaces()
        .assertTrue { it.hasPublicModifier() }
    Konist
        .scopeFromProject()
        .properties()
        .assertFalse { 
            it.hasAnnotationOf(Inject::class)
        }
    Konist
        .scopeFromProject()
        .properties()
        .assertFalse { 
            it.hasPublicOrDefaultModifier
        }
    Konist
        .scopeFromProject()
        .classes()
        .assertEmpty()
    Konist
        .scopeFromProject()
        .classes()
        .assertNotEmpty()
    Konist
        .scopeFromProject() 
        .classes()
        .assertFalse(strict = true) { ... }
    Konist
        .scopeFromProject() 
        .classes()
        .assertFalse(additionalMessage = "Do X to fix the issue") { ... }
    Konsist
        .scopeFromProject()
        .assertArchitecture { 
            // Assert architecture 
        }
    https://github.com/diffplug/spotless
    https://github.com/radarsh/gradle-test-logger-plugin
    https://junit.org/junit5/
    https://kotest.io/
    https://gradle.org/
    https://pinterest.github.io/ktlint/latest/
    https://github.com/detekt/detekt
    https://github.com/Kotlin/dokka

    dynamic

    Recommended

    To facilitate test name retrieval you can add this custom koTestName extension:

    JUnit 4 does not natively support dynamic tests; tests in this framework are typically static and determined at compile-time, so there is no need to pass testName argument.

    JUnit4

    static

    Not required

    JUnit5

    static

    Not required

    JUnit5

    dynamic

    Recommended

    Dynamic Konsist Tests
    JUnit 5
    JUnit 5
    Kotest
    @Test
    fun myTest() {
        Konsist.scopeFromProject()
            .classes()
            .assertTrue { ... }
    }
    class SampleDynamicKonsistTest {
        @TestFactory
        fun `use case test`(): Stream<DynamicTest> = Konsist
            .scopeFromProject()
            .classes()
            .withNameEndingWith("UseCase")
            .stream()
            .flatMap { useCase ->
                Stream.of(
                    dynamicTest("${useCase.name} should have test") {
                       useCase.assertTrue(testName = "${useCase.name} should have test") {
                            it.hasTestClass()
                        }
                    },
                    dynamicTest("${useCase.name} should reside in ..domain.usecase.. package") {
                        useCase.assertTrue(testName = "${useCase.name} should reside in ..domain.usecase.. package") {
                            it.resideInPackage("..domain.usecase..")
                        }
                    },
                )
            }
    }

    Kotest

    @Test
    fun myTest() {
        Konsist.scopeFromProject()
            .classes()
            .assertTrue { ... }
    }
    Konsist.scopeFromProject()
        .classes()
        .assertTrue(testName = "My test name") { ... } //passed test name
    class SampleDynamicKonsistTest : FreeSpec({
        Konsist
            .scopeFromProject()
            .classes()
            .withNameEndingWith("UseCase")
            .forEach { useCase ->
                "${useCase.name} should have test" {
                    useCase.assertTrue(testName = this.testCase.name.testName) { it.hasTestClass() }
                }
                "${useCase.name} should reside in ..domain.usecase.. package" {
                    useCase.assertTrue(testName = this.testCase.name.testName) { it.resideInPackage("..domain.usecase..") }
                }
            }
    })
    val TestScope.koTestName: String
        get() = this.testCase.name.testName

    The Kotest testing framework project dependency should be added to the project. See starter projects to get a complete sample project.

    Snippets
    Create The Scope
    Declaration
    Declaration Filtering
    Declaration Assertion
    Package Wildcard
    JUnit
    KoTest
    Pull Request
    JUnit
    starter projects
    Kotest Support
    https://github.com/LemonAppDev/konsist-documentation/blob/main/getting-started/getting-started/broken-reference/README.md
    Debug Konsist Test
    Dynamic Konsist Tests
    Layer
    language reference
    Create The Scope
    include
    Kotest
    starter projects

    Dynamic Konsist Tests

    From static to dynamic

    On this page, we explore the domain of static tests and then progress to the flexible world of dynamic tests. As a starting point, let's dive into the traditional approach of static Konsist tests.

    Why Use Dynamic Tests?

    With static tests, the failure is represented by a single test:

    From this failure, a developer discerns the breached rule and needs to dive into the test logs to determine the cause of the violation (to pinpoint the use case breaking the given rule).

    In contrast, dynamic tests immediately highlight the root issue since every use case is represented by its own distinct test:

    Utilizing dynamic tests over static ones makes it simpler to pinpoint failures. Consequently, it reduces the time and effort spent on parsing long error logs, offering a more efficient testing experience.

    Take a look at . Every and project has an additional dynamic test (SampleDynamicKonsistTest) preconfigured. Check out the project and run the test.

    Let's begin by creating a static test and then delve into the steps to transition towards dynamic tests.

    Static Tests

    Static tests are defined at compile-time. This means the structure and number of these tests are fixed when the code is compiled. When navigating the universe of Konsist tests, the standard approach is to execute several validations all bundled within a single test.

    To paint a clearer picture: imagine you have a rule (let's represent it with the tool icon 🛠️) ensuring that all use cases should be placed in a specific package. One static test (represented by the check icon ✅) can guard this rule, making sure that everything is in the right place:

    In most projects, the intricacy arises from a multitude of classes/interfaces, each with distinct duties. However, to simplify our understanding, let's use a straightforward and simplified example of a project with just three use cases:

    The goal is to verify if every use case follows these two rules:

    • verify if every use case has a test

    • verify if every use case is in domain.usecase package

    A typical approach would be to write two Konsist tests:

    Each rule is represented as a separate test verifying all of the use cases:

    Executing these tests will generate output in the IDE:

    While the current setup using static, predefined tests is functional, dynamic tests offer an avenue for improved development experience and flexibility.

    Dynamic Tests

    Dynamic tests are generated at runtime based on conditions and input data. In this scenario, the dynamic input data is the list of use cases that grows over the project life cycle.

    The objective is to generate dynamic tests for each combination of rule and use case (KoClass declaration) verified by Konsist. With three use cases and two rules for each, this will yield a total of six separate tests:

    Let's convert this idea into a dynamic test:

    JUnit provides built-in support for dynamic tests through its core framework. This ensures that developers can seamlessly employ dynamic testing capabilities.

    Theorg.junit.jupiter:junit-jupiter-params:x.v.z dependency is required to enable JUnit 5 dynamic tests.

    The IDE will display the tests as follows:

    Spring Snippets

    Konsist can be used to guard the consistency of the Spring project.

    1. Interfaces With Repository Annotation Should Have Repository Suffix

    2. Classes With RestController Annotation Should Have Controller Suffix

    3. Controllers Never Returns Collection Types

    4. Classes With RestController Annotation Should Reside In controller Package

    5. Classes With RestController Annotation Should Never Return Collection

    6. Service Classes Should Be Annotated With Service Annotation

    7. Entity Classes Should Have An Id Field

    8. DTO Classes Should Be Data Classes

    9. RestControllers Should Not Have State Fields

    10. Files With Domain Package Do Not Have Spring References

    11. Transactional Annotation Should Only Be Used On Default Or Public Methods That Are Not Part Of An Interface

    12. Every API Method In RestController With Admin Suffix Should Have PreAuthorize Annotation With ROLE_ADMIN

    13. Every Non-public Controller Should Have @PreAuthorize On Class Or On Each Endpoint Method

    Declaration Filtering

    Query and filter declarations using Konsist API

    Declaration Filtering

    Declaration querying allows to retrieval of declarations of a given type. It is the middle step of the Konsist config preceded by scope retrieval (Create The Scope) and followed by the verification (Declaration Assertion) step.

    Typically, verification has performed a collection of declarations such as methods marked with particular annotations or classes located within a single package.

    Every Create The Scope contains a set of declarations (Declaration) such as classes (KoClass), properties (KoProperty), functions (KoFunction), etc. The KoScope class provides a set of properties and methods to access Kotlin declarations. Each of them returns a list representing a declaration subset:

    To get all classes from the given scope use KoScope.classes() method:

    Here is an example of querying all properties defined inside classes:

    Filter Declarations

    More granular filtering can be applied to additionally filter classes annotated with certain attributes like classes annotated with UseCase annotation.

    Konsist is compatible with API, so the filter method can be used to filter the content of the List<KoClass>: Here filter return classes annotated with UseCase annotation:

    Konsist provides a set of with... extensions to simplify the filtering syntax. The above snippet can be improved:

    The.withAllAnnotationsOf(Annotation1::class, Annotation2::class) filter classes having all annotations present (Annotation1 and Annotation2).

    The.withSomeAnnotationsOf(Annotation1::class, Annotation2::class)

    Multiple conditions can be chained to perform more specific filtering. The below snippet filters classes with the BaseUseCase parent class that resides in the usecase package:

    It is also possible to filter declarations by using certain aspects e.g. visibility modifiers. Usage of providers allows verifying the visibility of different declaration types such as classes, functions, properties, etc:

    Query And Filter Declaration

    Querying and filtering stages can be mixed to perform more specific checks. The below snippet filters classes reside in the controller package retrieves all properties, and filters properties with Inject annotation:

    Print Declarations

    To print all declarations within use the print() method:

    Konsist
     // Define the scope containing all Kotlin files present in the project
    Konsist.scopeFromProject() //Returns KoScope
    Konsist.scopeFromProject()
        // Get scope classes
        .classes() 
    
    Konsist.scopeFromProject()
        .classes()
        // Filter classes annotated with 'RestController'
        .withAllAnnotationsOf(RestController::class) 
    Konsist.scopeFromProject()
        .classes()
        .withAllAnnotationsOf(RestController::class)
        .assertTrue { 
            // Define the assertion
        } 
    Konsist.scopeFromProject()
        .classes()
        .withAllAnnotationsOf(RestController::class)
        .assertTrue { 
           // Check if classes are located in the controller package
            it.resideInPackage("..controller") 
        } 
    class ControllerClassKonsistTest {
        @Test
        fun `classes annotated with 'RestController' annotation reside in 'controller' package`() {
          // 1. Create a scope representing the whole project (all Kotlin files in project)
                Konsist.scopeFromProject()
                // 2. Retrieve class declarations
                .classes()
                // 3. Filter classes annotated with 'RestController'
                .withAllAnnotationsOf(RestController::class)
                // 4. Define the assertion
                .assertTrue { it.resideInPackage("..controller..") }
        }
    }
    class ControllerClassKonsistTest : FreeSpec({
        "classes annotated with 'RestController' annotation reside in 'controller' package" {
             Konsist
                // 1. Create a scope representing the whole project (all Kotlin files in project)
                .scopeFromProject()
                // 2. Retrieve class declarations
                .classes() // 2. Get scope classes
                // 3. Filter classes annotated with 'RestController'
                .withAllAnnotationsOf(RestController::class)
                // 4. Define the assertion
                .assertTrue (testName = this.testCase.name.testName) { 
                    it.resideInPackage("..controller..") 
                }
        }
    })
    Konsist
        .scopeFromProject()
        .assertArchitecture {
            // Define layers
            val presentation = Layer("Presentation", "com.myapp.presentation..")
            val data = Layer("Data", "com.myapp.data..")
        }
    Konsist
        .scopeFromProject()
        .assertArchitecture {
            val presentation = Layer("Presentation", "com.myapp.presentation..")
            val data = Layer("Data", "com.myapp.data..")
    
            // Define dependencies 
            presentation.dependsOn(data)
            data.dependsOnNothing()
        }
    // Optional dependency - Feature layer may depend on Domain layer
    featureLayer.dependsOn(domainLayer) // strict = false by default
    
    // Required dependency - Feature layer must depend on Domain layer
    featureLayer.dependsOn(domainLayer, strict = true)
    Konsist
        .scopeFromProject()
        .files
        .withNameStartingWith("Repository")
        .assertArchitecture {
            val presentation = Layer("Presentation", "com.myapp.presentation..")
            val data = Layer("Data", "com.myapp.data..")
    
            presentation.dependsOn(data)
            data.dependsOnNothing()
        }
    private val domain = Layer("Domain",  "com.domain..")
    private val presentation = Layer("Presentation", "com..presentation..")
    
    Konsist
        .scopeFromProject()
        scope.assertArchitecture {
            // Include presentation for architectural check without defining a dependency
            presentation.include()
            
            // Include domain layer or architectural check and define no dependency (independent)
            domain.doesOnNothing()
        }
    }
    // Define architecture
    val architecture = architecture {
            val presentation = Layer("Presentation", "com.myapp.presentation..")
            val data = Layer("Data", "com.myapp.data..")
    
            presentation.dependsOn(data)
            data.dependsOnNothing()
    }
    
    // Assert Architecture of two modules using common architecture rules
    moduleFeature1Scope.assertArchitecture(architecture)
    moduleFeature2Scope.assertArchitecture(architecture)
    class ArchitectureTest {
        private val architecture = architecture {
            val presentation = Layer("Presentation", "com.myapp.presentation..")
            val data = Layer("Data", "com.myapp.data..")
    
            presentation.dependsOn(data)
            data.dependsOnNothing()
        }
    
        @Test
        fun `architecture layers of feature1 module have dependencies correct`() {
            moduleFeature1Scope.assertArchitecture(architecture)
        }
        
        @Test
        fun `architecture layers of feature2 module have dependencies correct`() {
            moduleFeature2Scope.assertArchitecture(architecture)
        }
    }
    @Test
    fun `interfaces with 'Repository' annotation should have 'Repository' suffix`() {
        Konsist
            .scopeFromProject()
            .interfaces()
            .withAnnotationOf(Repository::class)
            .assertTrue { it.hasNameEndingWith("Repository") }
    }
    @Test
    fun `classes with 'RestController' annotation should have 'Controller' suffix`() {
        Konsist
            .scopeFromProject()
            .classes()
            .withAnnotationOf(RestController::class)
            .assertTrue { it.hasNameEndingWith("Controller") }
    }
    @Test
    fun `controllers never returns collection types`() {
        /*
        Avoid returning collection types directly. Structuring the response as
        an object that contains a collection field is preferred. This approach
        allows for future expansion (e.g., adding more properties like "totalPages")
        without disrupting the existing API contract, which would happen if a JSON
        array were returned directly.
        */
        Konsist
            .scopeFromPackage("story.controller..")
            .classes()
            .withAnnotationOf(RestController::class)
            .functions()
            .assertFalse { function ->
                function.hasReturnType { it.isKotlinCollectionType }
            }
    }
    @Test
    fun `classes with 'RestController' annotation should reside in 'controller' package`() {
        Konsist
            .scopeFromProject()
            .classes()
            .withAnnotationOf(RestController::class)
            .assertTrue { it.resideInPackage("..controller..") }
    }
    @Test
    fun `classes with 'RestController' annotation should never return collection`() {
        Konsist
            .scopeFromPackage("story.controller..")
            .classes()
            .withAnnotationOf(RestController::class)
            .functions()
            .assertFalse { function ->
                function.hasReturnType { it.hasNameStartingWith("List") }
            }
    }
    @Test
    fun `Service classes should be annotated with Service annotation`() {
        Konsist
            .scopeFromProject()
            .classes()
            .withNameEndingWith("Service")
            .assertTrue { it.hasAnnotationOf(Service::class) }
    }
    @Test
    fun `Entity classes should have an Id field`() {
        Konsist
            .scopeFromProject()
            .classes()
            .withAnnotationOf(Entity::class)
            .assertTrue { clazz ->
                clazz.properties().any { property ->
                    property.hasAnnotationOf(Id::class)
                }
            }
    }
    @Test
    fun `DTO classes should be data classes`() {
        Konsist
            .scopeFromProject()
            .classes()
            .withNameEndingWith("DTO")
            .assertTrue { it.hasModifier(KoModifier.DATA) }
    }
    @Test
    fun `RestControllers should not have state fields`() {
        Konsist
            .scopeFromProject()
            .classes()
            .withAnnotationOf(RestController::class)
            .objects()
            .withModifier(KoModifier.COMPANION)
            .assertTrue {
                it.properties().isEmpty()
            }
    }
    @Test
    fun `files with domain package do not have Spring references`() {
        Konsist.scopeFromProduction()
            .files
            .withPackage("..domain..")
            .assertFalse {
                it
                    .imports
                    .any { import ->
                        import.name.startsWith("org.springframework")
                    }
            }
    }
    @Test
    fun `Transactional annotation should only be used on default or public methods that are not part of an interface`() {
        Konsist.scopeFromProject()
            .functions()
            .withAnnotationOf(Transactional::class)
            .assertTrue {
                it.hasPublicOrDefaultModifier && it.containingDeclaration !is KoInterfaceDeclaration
            }
    }
    @Test
    fun `every API method in RestController with 'Admin' suffix should have PreAuthorize annotation with ROLE_ADMIN`() {
        Konsist.scopeFromProject()
            .classes()
            .withAnnotationOf(RestController::class)
            .withNameEndingWith("Admin")
            .functions()
            .assertTrue {
                it.hasAnnotationOf(PreAuthorize::class) && it.text.contains("hasRole('ROLE_ADMIN')")
            }
    }
    @Test
    fun `every non-public Controller should have @PreAuthorize on class or on each endpoint method`() {
        Konsist.scopeFromProject()
            .classes()
            .withAnnotationOf(RestController::class)
            .filterNot { it.hasPublicModifier }
            .assertTrue { controller ->
                controller.hasAnnotationOf(PreAuthorize::class) ||
                        controller.functions()
                            .all { it.hasAnnotationOf(PreAuthorize::class) }
            }
    }

    For dynamic tests such as JUnit 5, it is recommended that the test name is explicitly provided using testName argument (see Explicit Test Names). At the moment test names are duplicated. This aspect has to be further investigated.

    Kotest offers native support for JUnit's dynamic tests. Developers can effortlessly integrate and utilize dynamic testing features without needing additional configurations or plugins.

    The IDE will display the tests as follows:

    For dynamic tests such as Kotest, it is recommended that the test name is explicitly provided using testName argument (see Explicit Test Names).

    In JUnit 4, the concept of dynamic tests (like JUnit 5's @TestFactory) does not exist natively thus dynamic tests are not supported.

    sample projects
    JUnit5
    Kotest

    returns all functions present in the scope

    properties()

    returns all properties present in the scope

    typeAliases

    returns all type aliases present in the scope

    declarations()

    returns all declarations present in the scope

    filter classes having at least one annotation (
    Annotation1
    or
    Annotation2
    )
    .

    Method

    Description

    files

    returns all files present in the scope

    packages

    returns all packages present in the scope

    imports

    returns all imports present in the scope

    classes()

    returns all classes present in the scope

    interfaces()

    returns all interfaces present in the scope

    objects()

    returns all objects present in the scope

    Kotlin Collection processing

    functions()

    General Snippets

    1. Files In ext Package Must Have Name Ending With Ext

    2. All Data Class Properties Are Defined In Constructor

    3. Every Class Has Test

    4. Every Class - Except Data And Value Class - Has Test

    5. Properties Are Declared Before Functions

    6. Every Constructor Parameter Has Name Derived From Parameter Type

    7. Every Class Constructor Has Alphabetically Ordered Parameters

    8. Enums Has Alphabetically Ordered Consts

    9. Companion Object Is Last Declaration In The Class

    10. Every Value Class Has Parameter Named value

    11. No Empty Files Allowed

    12. No Field Should Have m Prefix

    13. No Class Should Use Field Injection

    14. No Class Should Use Java Util Logging

    15. Package Name Must Match File Path

    16. No Wildcard Imports Allowed

    17. Forbid The Usage Of forbiddenString In File

    18. All Function Parameters Are Interfaces

    19. All Parent Interfaces Are Public

    20. Return Type Of All Functions Are Immutable

    Declaration References

    Declaration reference represents a link between codebase declarations. Konsist allows to precisely verify properties of linked type. This type can be used in function or property declaration or child/parent class or interface. For example

    1. Verify if all types of function parameters are interfaces:

    2. Access properties of parents (parent classes and child interfaces). Below snippet checks if parent class has internal modifier:

    3. Access properties of children (child classes and child interfaces). Below snippet checks if all interfaces have children that resided in ..somepackage.. package:

    Type Representation

    Kotlin types can defined in multiple ways. Consider foo property with Foo type:

    The Foo type can be defined by:

    • class

    • interface

    • object

    • type alias

    The Foo type can be represented by one of KoXDeclaration classes:

    Sorce
    Declaration

    Each of these types possesses a largely distinct set of characteristics; for instance, classes and interfaces can include annotations, whereas import aliases cannot.

    To access properties the specific declaration type, the declaration cast to more specific type is required (from generic KoTypeDeclaration). Example below assumes that Foo is represented by the Foo class:

    To facilitate testing Konsist API provides set of dedicated casting extensions. The above code can be simplified:

    Here is the list of all casting extensions:

    Sorce
    Declaration
    Cast Extension
    Type Check Extension

    Type Represented By Class

    Source code:

    Usage:

    Konsist test:

    Type Represented By Interface

    Source code:

    Usage:

    Konsist test:

    Type Represented By Object

    This scenario is uncommon, but still possible.

    Source code:

    Usage:

    Konsist test:

    Type Represented By Type Alias

    Source code:

    Usage:

    Konsist test:

    Type Represented By Import Alias

    Source code:

    Usage:

    Konsist test:

    Type Represented By Kotlin Type

    Source code:

    Usage:

    Konsist test:

    Type Represented Function Type

    Source code:

    Usage:

    Konsist test:

    Type Represented By External Type

    External type represents the type defined outside of the project codebase, usually by external library. Konsist is not able to parse this type, so type information is limited (Konsist is not able to parse the compiled file).

    For Example:

    The Android ViewModel class is provided by androidx.lifecycle:lifecycle-viewmodel-ktx dependency, so Konsist has limited information.

    class UseCaseKonsistTest : FreeSpec({
        Konsist
            .scopeFromProject()
            .classes()
            .withNameEndingWith("UseCase")
            .forEach { useCase ->
                "${useCase.name} should have test" {
                    useCase.assertTrue(testName = this.testCase.name.testName) { it.hasTestClass() }
                }
                "${useCase.name} should reside in ..domain.usecase.. package" {
                    useCase.assertTrue(testName = this.testCase.name.testName) { it.resideInPackage("..domain..usecase..") }
                }
            }
    })
    class UseCaseKonsistTest {
        @Test
        fun `use case should have test`() {
            Konsist
                .scopeFromProject()
                .classes()
                .withNameEndingWith("UseCase")
                .assertTrue { it.hasTestClass() }
        }
    
        @Test
        fun `use case reside in domain dor usecase package`() {
            Konsist
                .scopeFromProject()
                .classes()
                .withNameEndingWith("UseCase")
                .assertTrue { it.resideInPackage("..domain..usecase..") }
        }
    }
    class UseCaseKonsistTest : FreeSpec({
        val useCases = Konsist
            .scopeFromProject()
            .classes()
            .withNameEndingWith("UseCase")
    
        "use case should have test" {
            useCases.assertTrue(testName = this.testCase.name.testName) { it.hasTestClass() }
        }
    
        "use case should reside in ..domain.usecase.. package" {
            useCases.assertTrue(testName = this.testCase.name.testName) { it.resideInPackage("..domain.usecase..") }
        }
    })
    class UseCaseKonsistTest {
        @TestFactory
        fun `use case test`(): Stream<DynamicTest> = Konsist
            .scopeFromProject()
            .classes()
            .withNameEndingWith("UseCase")
            .stream()
            .flatMap { useCase ->
                Stream.of(
                    dynamicTest("${useCase.name} should have test") {
                        useCase.assertTrue(testName = "${useCase.name} should have test") {
                            it.hasTestClass()
                        }
                    },
                    dynamicTest("${useCase.name} should reside in ..domain.usecase.. package") {
                        useCase.assertTrue(testName = "${useCase.name} should reside in ..domain.usecase.. package") {
                            it.resideInPackage("..domain.usecase..")
                        }
                    },
                )
            }
    }
    koScope
        .classes()
        koScope
            .classes()
            .properties()
            .assertTrue { 
                //...
            }
    koScope
        .classes()
        .filter { it.hasAnnotationOf<UseCase>() }
        .assertTrue { 
            //... 
        }
    koScope
        .classes()
        .withAllAnnotationsOf(UseCase::class)
        .assertTrue { 
            //...
        }
    koScope
        .classes()
        .withAllAnnotationsOf(UseCase::class)
        .withPackage("..usecase")
        .assertTrue { 
            //...
        }
    koScope
        .declarationsOf<KoVisibilityModifierProvider>()
        .assertTrue { it.hasInternalModifier }
    koScope
        .classes() // query all classes
        .withPackage("..controller") // filter classes in 'controller' package
        .properties()  // query all properties
        .withAnnotationOf(Inject::class) // filter classes in 'controller' package
        .assertTrue { 
            //...
        }
    koScope
        .classes()
        .properties()
        .print()
    @Test
    fun `files in 'ext' package must have name ending with 'Ext'`() {
        Konsist
            .scopeFromProject()
            .files
            .withPackage("..ext..")
            .assertTrue { it.hasNameEndingWith("Ext") }
    }
    Konsist
        .scopeFromProject()
        .functions()
        .parameters
        .types
        .assertTrue {
            it.isInterface
        }
    fun `all parrent interfaces are internal`() {
        Konsist
            .scopeFromProject()
            .classes()
            .parentInterfaces()
            .assertTrue {
                it.hasInternalModifier()
            }
    }
    Konsist
        .scopeFromProject()
        .interfaces()
        .assertTrue {
            it.hasAllChildren(indirectChildren = true) { child -> 
                child.resideInPackage("..somepackage..") 
            }
        }
    @Test
    fun `all data class properties are defined in constructor`() {
        Konsist
            .scopeFromProject()
            .classes()
            .withModifier(KoModifier.DATA)
            .properties()
            .assertTrue { it.isConstructorDefined }
    }
    @Test
    fun `every class has test`() {
        Konsist
            .scopeFromProduction()
            .classes()
            .assertTrue { it.hasTestClasses() }
    }
    @Test
    fun `every class - except data and value class - has test`() {
        Konsist
            .scopeFromProduction()
            .classes()
            .withoutModifier(KoModifier.DATA, KoModifier.VALUE)
            .assertTrue { it.hasTestClasses() }
    }
    @Test
    fun `properties are declared before functions`() {
        Konsist
            .scopeFromProject()
            .classes()
            .assertTrue {
                val lastKoPropertyDeclarationIndex = it
                    .declarations(includeNested = false, includeLocal = false)
                    .indexOfLastInstance<KoPropertyDeclaration>()
    
                val firstKoFunctionDeclarationIndex = it
                    .declarations(includeNested = false, includeLocal = false)
                    .indexOfFirstInstance<KoFunctionDeclaration>()
    
                if (lastKoPropertyDeclarationIndex != -1 && firstKoFunctionDeclarationIndex != -1) {
                    lastKoPropertyDeclarationIndex < firstKoFunctionDeclarationIndex
                } else {
                    true
                }
            }
    }
    @Test
    fun `every constructor parameter has name derived from parameter type`() {
        Konsist
            .scopeFromProject()
            .classes()
            .constructors
            .parameters
            .assertTrue {
                val nameTitleCase = it.name.replaceFirstChar { char -> char.titlecase(Locale.getDefault()) }
                nameTitleCase == it.type.sourceType
            }
    }
    @Test
    fun `every class constructor has alphabetically ordered parameters`() {
        Konsist
            .scopeFromProject()
            .classes()
            .constructors
            .assertTrue { it.parameters.isSortedByName() }
    }
    @Test
    fun `enums has alphabetically ordered consts`() {
        Konsist
            .scopeFromProduction()
            .classes()
            .withAllModifiers(KoModifier.ENUM)
            .assertTrue { it.enumConstants.isSortedByName() }
    }
    @Test
    fun `companion object is last declaration in the class`() {
        Konsist
            .scopeFromProject()
            .classes()
            .assertTrue {
                val companionObject = it.objects(includeNested = false).lastOrNull { obj ->
                    obj.hasModifier(KoModifier.COMPANION)
                }
    
                if (companionObject != null) {
                    it.declarations(includeNested = false, includeLocal = false).last() == companionObject
                } else {
                    true
                }
            }
    }
    @Test
    fun `every value class has parameter named 'value'`() {
        Konsist
            .scopeFromProject()
            .classes()
            .withValueModifier()
            .primaryConstructors
            .assertTrue { it.hasParameterWithName("value") }
    }
    @Test
    fun `no empty files allowed`() {
        Konsist
            .scopeFromProject()
            .files
            .assertFalse { it.text.isEmpty() }
    }
    @Test
    fun `no field should have 'm' prefix`() {
        Konsist
            .scopeFromProject()
            .classes()
            .properties()
            .assertFalse {
                val secondCharacterIsUppercase = it.name.getOrNull(1)?.isUpperCase() ?: false
                it.name.startsWith('m') && secondCharacterIsUppercase
            }
    }
    @Test
    fun `no class should use field injection`() {
        Konsist
            .scopeFromProject()
            .classes()
            .properties()
            .assertFalse { it.hasAnnotationOf<Inject>() }
    }
    @Test
    fun `no class should use Java util logging`() {
        Konsist
            .scopeFromProject()
            .files
            .assertFalse { it.hasImport { import -> import.name == "java.util.logging.." } }
    }
    @Test
    fun `package name must match file path`() {
        Konsist
            .scopeFromProject()
            .packages
            .assertTrue { it.hasMatchingPath }
    }
    @Test
    fun `no wildcard imports allowed`() {
        Konsist
            .scopeFromProject()
            .imports
            .assertFalse { it.isWildcard }
    }
    @Test
    fun `forbid the usage of 'forbiddenString' in file`() {
        Konsist
            .scopeFromProject()
            .files
            .assertFalse { it.hasTextContaining("forbiddenString") }
    }
    @Test
    fun `all function parameters are interfaces`() {
        Konsist
            .scopeFromProject()
            .functions()
            .parameters
            .types
            .assertTrue { it.sourceDeclaration?.isInterface }
    }
    @Test
    fun `all parent interfaces are public`() {
        Konsist
            .scopeFromProject()
            .classes()
            .parentInterfaces()
            .sourceDeclarations()
            .interfaceDeclarations()
            .assertTrue { it.hasPublicModifier }
    }
    @Test
    fun `return type of all functions are immutable`() {
        Konsist
            .scopeFromProject()
            .functions()
            .returnTypes
            .assertFalse { it.isMutableType }
    }
    import alias
  • kotlin types (Kotlin basic type or Kotlin collections type)

  • function type

  • external library (type defined outside project codebase) represents declaration which is not defined in the project

  • KoFunctionDeclaration

    KoExternalDeclaration

    KoTypeAliasDeclaration

    asTypeAliasDeclaration

    isTypeAlias

    KoImportAliasDeclaration

    asImportAliasDeclaration

    isImportAlias

    KoKotlinTypeDeclaration

    asKotlinTypeDeclaration

    isKotlinType

    KoFunctionDeclaration

    asFunctionTypeDeclaration

    isFunctionType

    KoExternalDeclaration

    asExternalTypeDeclaration

    isExternalType

    Type Represented By Class

    KoClassDeclaration

    Type Represented By Interface

    KoInterfaceDeclaration

    Type Represented By Object

    KoObjectDeclaration

    Declaration References

    KoTypeAliasDeclaration

    Type Represented By Import Alias

    KoImportAliasDeclaration

    Type Represented By Kotlin Type

    KoKotlinTypeDeclaration

    Type Represented By Class

    KoClassDeclaration

    asClassDeclaration

    isClass

    Type Represented By Interface

    KoInterfaceDeclaration

    asObjectDeclaration

    isObject

    Type Represented By Object

    KoObjectDeclaration

    asInterfaceDeclaration

    isInterface

    Contributing

    Let's Improve Konsist Together

    General

    So you want to help? That's great!

    To chat with Konsist developers and the Konsist community please check the #konsist channel at kotlinlang Slack workspace (preferred), or start a new .

    The Konsist project is now at a critical stage where community input is essential to polish and mature it.

    There are a variety of ways to contribute to the Konsit project:

    • Coding: This is the most common way to contribute. You can fix bugs or add new features.

    • Testing: You can help to improve the quality by testing the code and reporting bugs. This is a great way to get involved and help out maturing the project.

    • Documentation: You can help to improve the documentation by writing or editing documentation. This is a great way to help people understand how to use Konsist.

    No matter how you choose to contribute, you will be making a valuable contribution to the open-source community.

    Contributing

    Our in JIRA.

    The best way to interact with the Konsist team is the dedicated channel (). If you want to help or need guidelines just say hello at Slack channel.

    Tickets that can be grabbed by the community have a label. You can also work on another improvement or bug-fix, but this may require more alignment, for example, certain features and planned ahead, so the ticket should be completed within a given time period.

    Start Contributing - Konsist

    1. Get contributor JIRA access - send your email in DM to at .

    2. Pick the ticket in JIRA

    3. Assign it to yourself, and update the ticket status to In Progress

    4. Fork repository (uncheck "Copy the main branch only")

    1. Branch of branch

    2. Implement the changes

    3. Add tests (look around in codebase for similar code being tested)

    4. Open draft with branch as target ( branch will be merged into the branch after the release)

    Start Contributing - Konsist Docs

    The - repository contains Konsist documentation (this webpage).

    1. Fork repository

    2. Branch of branch

    3. Make changes

    4. Open with branch as a target

    Checks

    During the PR review, several types of checks are executed using (). These checks can also be executed locally using the following commands:

    • (runs )

      • ./gradlew spotlessCheck - check the code using Spotless

      • ./gradlew spotlessApply - check and fix code using Spotless (if possible)

    Konsist adheres to stringent testing standards. Each Provider undergoes testing against every type of declaration, leading to an extensive set of tests. This thorough testing ensures two main objectives:

    1. Guaranteeing future compatibility with Kotlin 2.0.

    2. Due to reliance on an external library for parsing, it's imperative to have comprehensive tests to ensure the Konsist API functions as anticipated.

    IntelliJ IDEA Plugins

    Some of the project README files contain diagrams. For a diagram preview, it is recommended to install the .

    Testing Changes Locally

    Publish Konsist Artifact To Local Maven Repository

    To test the changes locally you can publish a SNAPSHOT artifact of the Konsist to the local maven repository:

    After publishing a new artifact x.y.z-SNAPSHOT with the version number will appear in the local Maven repository:

    The actual Konsist version is defined in the file. The SNAPSHOT suffix will be added automatically to the published artifact.

    To use this artifact you have to add a local Maven repository to your project.

    Use Published Artifact From Local Maven Repository

    Every project contains a list of the repositories used to retrieve the dependencies. A local Maven repository has to be manually added to the project.

    Add the following block to the build.gradle / build.gradle.kts file:

    By default, the Maven project uses a local repository. If not add the following block to the module\pom.xml file:

    Dependency can be added to other build systems as well. Check the section in the sonatype repository.

    Now build scripts will use the local repository to resolve dependencies, however, the version of Konsist has to be updated to the SNAPSHOT version of the newly published artifact e.g.

    com.lemonappdev:konsist:0.12.0-SNAPSHOT

    Now build scripts will be able to resolve this newly published Konsist artifact.

    Verify Used Konsist Artifact Version

    IntelliJ IDEA UI provides a convenient way to check which version of Konsist is used by the project. Open the External Libraries section of Project view and search for Konsist dependency:

    No Matching Toolchains Found Error

    If during a build you encounter an error regarding No matching toolchains found then open Module Settings / Project Structure windows and set Java SDK to version e.g. 19.

    You can install missing JDKs directly from IntelliJ IDEA - click on the Module SDK combo box and select +Add SDK.

    If during the build you encounter an error regarding Could not determine the dependencies of null. then open File / Settings / Build, Execute, Deployment / Build Tools / Gradle window and set Java SDK to version 19.

    Architecture

    Source Sets

    Konsist contains multiple custom source sets (defined by the ) to provide better isolation between various types of tests:

    • test - tests related to generic Konsist API (everything except the architectureAssert)

    • apiTest - tests related to architectureAssert

    • integrationTest

    We aim to test the majority of aspects within these source sets. However, certain kinds of checks require a dedicated test project. These projects are available in the directory on the Konsist repository.

    Layers

    The high-level view of Konsist architecture:

    Make a Change In The Konsist Documentation Repository

    The repository contains this website. Create a fork of the repository, make changes using any text editor (e.g. ), and open the Pull Request targeting the main branch.

    Updating Snippets

    The section requires a different approach. To ensure the snippets remain valid and aligned with Konsist API, we store them within the of the repository. With every release, new snippet pages are generated from the and placed in the GitBook documentation ( repository).

    Some snippets depend on classes/interfaces/annotations from external frameworks such as Spring Repository annotation or Android ViewModel class. To avoid coupling Konsist with these frameworks and allow snippet compilation, we store placeholder classes mimicking the full names of the external framework in . class e.g. .

    Isolate Konsist Tests

    Aim for better test separation.

    Typically, it's advisable to consolidate all Konsist tests in a unified location. This approach is preferred because these tests are often designed to validate the structure of the entire project's codebase. There are three potential options for storing Konsist tests in project codebase:

    Android
    Spring
    KMP
    Pure Kotlin

    ✅

    ✅

    ✅

    ✅

    Recommended approach is to use or a . These approaches allows to easily isolate Konsist tests from other types of tests e.g. separate unit tests from Konsist tests.

    Existing Test Source Set

    The Konsist library can be added to the project by adding the dependency on the existing test source set .

    To execute tests run ./gradlew test command.

    The downside of this approach is that various types of tests are mixed in test source set e.g. unit tests and Konsist tests.

    Dedicated konsistTest Source Set

    This section demonstrates how to add the konsistTest test source directory inside the app module. This configuration is mostly useful for Spring and Kotlin projects.

    This page describes the test located in the app module with the build config file located in app a folder. If the project does not contain any module then configuration should be applied in the root build config file.

    This test directory will have a kotlin folder containing Kotlin code.

    Use the Gradle built-in to define the konsistTest source set. Add a testing block to the project configuration:

    Use the Gradle built-in to define the konsistTest source set. Add a testing block to the project configuration:

    Use the to define the konsistTest test source directory. Add plugin config to the project configuration:

    Create app/src/konsistTest/kotlin folder and reload the project. The IDE will present a new konsistTest source set in the app module.

    The konsistTest test source folder works exactly like the build-in test source folder, so Kosist tests can be defined and executed in a similar way:

    Dedicated Gradle Module

    This section demonstrates how to add the konsistTest module to the project. This configuration is primarily helpful for Android projects and Kotlin Multiplatform (KMP) projects, however, this approach will also work with Spring and pure Kotlin projects.

    The is used to build Android apps. The Android Gradle Plugin is not compatible with the and it does not allow adding new source sets. To fully isolate tests a new module is required.

    The project contains modules with code for different platforms. To decouple Konsist tests from a single platform dedicated module containing Konsist test should be added.

    Add Gradle konsistTest Module:

    Create konsistTest/src/test/kotlin directory in the project root:

    Add module include inside settings.gradle.kts file:

    Create konsistTest/src/test/kotlin directory in the project root:

    Add module include inside settings.gradle.kts file:

    Running Konsist Tests Stored In A Dedicated Gradle Module

    Gradle's default behavior assumes that a module's code is up-to-date if the module itself hasn't been modified. This can lead to issues when Konsist tests are placed in a separate module. In such cases, Gradle may skip these tests, believing they're unnecessary.

    However, this approach doesn't align well with Konsist's functionality. Konsist analyzes the entire codebase, not just individual modules. As a result, when Gradle skips Konsist tests based on its module-level change detection, it fails to account for potential changes in other modules that Konsist would typically examine.

    There are few solutions to this problem.

    Solution 1: Module Is Always Out of Date

    An alternative solution for this problem is to define konsistTest module as always being out of date:

    Solution 2: Flag --rerun-tasks

    To execute all unit tests besides tests in the konsistTest module run:

    ./gradlew test -x konsistTest:test

    To avoid manually passing --rerun-tasks flag each time a custom konsistCheck task can be added to the root build config file:

    Add to root build.gradle.kts:

    Add to root build.gradle:

    After adding konsistCheck task run ./gradlew konsistCheck to execute all Konsist tests.

    val foo: Foo
    Konsist
        .scopeFromProject()
        .properties()    
        .types
        .assertTrue { koTypeDeclaration ->
            val koClass = koTypeDeclaration as KoClassDeclaration
    
            koClass.hasAllAnnotations {
                it.representsTypeOf<String>()
            }
        }
    Konsist
        .scopeFromProject()
        .properties()
        .types
        .assertTrue { koTypeDeclaration ->
            koTypeDeclaration
            .asClassDeclaration
            ?.hasAllAnnotations {
                it.representsTypeOf<String>()
            }
        }
    internal class Foo
    val foo: Foo? = null
    scope
        .properties()
        .types
        .assertTrue {
            it.asClassDeclaration?.hasInternalModifier
        }
    internal interface Foo
    val foo: Foo? = null
    scope
        .properties()
        .types
        .assertTrue {
            it.asInterfaceDeclaration?.hasInternalModifier
        }
    internal object Foo
    val foo: Foo? = null
    scope
        .properties()
        .types
        .assertTrue {
            it.asObjectDeclaration?.hasInternalModifier
        }
    internal object Foo
    typealias MyFoo = Foo
    val foo: MyFoo? = null
    scope
        .properties()
        .types
        .assertTrue {
            it
                .sourceTypeAlias
                .type
                .sourceInterface
                .hasInternalModifier
        }
    internal object Foo
    import com.app.Foo as MyFoo
    
    val foo: MyFoo? = null
    scope
        .properties()
        .types
        .assertTrue {
            it
                .asTypeAliasDeclaration
                .type
                .asInterfaceDeclaration
                .hasInternalModifier
        }
    // Kotlin internal source code for String
    val foo: String? = null
    scope
        .properties()
        .types
        .assertTrue {
            it.asKotlinTypeDeclaration.name == "String"
        }
    // Kotlin internal source code
    val foo: () -> Unit? = null
    scope
        .properties()
        .types
        .assertTrue {
            it
                .sourceFunctionType
                .parameterTypes
                .isEmpty()
        }
    class MyViewModel: ViewModel
    Type Represented Function Type
    Type Represented By External Type
    Declaration References
    Type Represented By Kotlin Type
    Type Represented Function Type
    Type Represented By External Type
    Declaration References
    For Android projects add com.android.library plugin in the konsistTest/scr/test/kotlin/build.gradle file.

    Refresh/Sync the Gradle Project in IDE.

    Isolate Konsist Tests

    ❌

    ✅

    ✅

    ✅

    Isolate Konsist Tests

    ✅

    ✅

    ✅

    ✅

    JVM Test Suite Plugin
    JVM Test Suite Plugin
    Maven Build Helper Plugin
    Android Gradle Plugin
    JVM Test Suite Plugin
    Kotlin Multiplatform
    test sorce directory
    konsistTest sorce directory
    Dedicated konsistTest Source Set
    Isolate Konsist Tests
    Existing Test Source Set
    // build.gradle.kts (root)
    
    plugins {
        `jvm-test-suite`
    }
    
    testing {
        suites {
            register("konsistTest", JvmTestSuite::class) {
                dependencies {
                    // Add 'main' source set dependency
                    implementation(project())
                    
                    // Add Konsist dependency
                    implementation("com.lemonappdev:konsist:0.13.0") 
                }
            }
        }
    }
    
    // Optional : Remove Konsist tests from the 'check' task if it exists
    tasks.matching { it.name == "check" }.configureEach {
      setDependsOn(dependsOn.filter { it.toString() != "konsistTest" })
    }
    // build.gradle (root)
    
    plugins {
        id 'jvm-test-suite'
    }
    
    testing {
        suites { 
            test { 
                useJUnitJupiter() 
            }
    
            konsistTest(JvmTestSuite) { 
                dependencies {
                    // Add 'main' source set dependency
                    implementation project() 
                    
                    // Add Konsist dependency
                    implementation "com.lemonappdev:konsist:0.13.0"
                }
    
                targets { 
                    all {
                        testTask.configure {
                            shouldRunAfter(test)
                        }
                    }
                }
            }
        }
    }
    
    // Optional: Remove Konsist tests from the 'check' task if it exists
    tasks.matching { it.name == "check" }.configureEach { task ->
        task.setDependsOn(task.getDependsOn().findAll { it.toString() != "konsistTest" })
    }
    # app/pom.xml
    
    <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>build-helper-maven-plugin</artifactId>
        <version>3.3.0</version>
        <executions>
            <execution>
                <id>add-konsist-test-source</id>
                <phase>generate-test-sources</phase>
                <goals>
                    <goal>add-test-source</goal>
                </goals>
                <configuration>
                    <sources>
                        <source>${project.basedir}/src/konsistTest/kotlin</source>
                    </sources>
                </configuration>
            </execution>
        </executions>
    </plugin>
    ./gradlew app:konsistTest
    mvn test
    // settings.gradle.kts
    include(":konsistTest")
    // settings.gradle
    include ':konsistTest'
    // konsistTest/build.gradle.kts
    
    tasks.withType<Test> {
        outputs.upToDateWhen { false }
    }
    // konsistTest/build.gradle
    
    tasks.withType(Test) {
        outputs.upToDateWhen { false }
    }
    tasks.register("konsistCheck") {
        group = "verification"
        description = "Runs Konsist static code analysis"
    
        doLast {
            val output = ByteArrayOutputStream()
            val result = project.exec {
                commandLine("./gradlew", "konsistTest:test", "--rerun-tasks")
                standardOutput = output
                errorOutput = output
                isIgnoreExitValue = true
            }
    
            println(output.toString())
    
            if (result.exitValue != 0) {
                throw GradleException("Konsist tests failed")
            }
        }
    }
    tasks.register("konsistCheck") {
        group = "verification"
        description = "Runs Konsist static code analysis"
    
        doLast {
            def output = new ByteArrayOutputStream()
            def result = project . exec {
                commandLine './gradlew', 'konsistTest:test', '--rerun-tasks'
                standardOutput = output
                errorOutput = output
                ignoreExitValue = true
            }
    
            println output . toString ()
    
            if (result.exitValue != 0) {
                throw new GradleException ("Konsist tests failed")
            }
        }
    }
    Community: You can answer questions or participate in discussions (GitHub, Slack). This is a great way to connect with other programmers.
  • Spread the word: You can help to spread the word about the Konsist by talking about it with fellow developers. You can also write a short post or a full-fledged article. Make sure to let us know at #konsist channel if you do so.

  • Make sure all checks are passing before marking PR as Ready for review.

    Detekt
    • ./gradlew detektCheck - check the code using Detekt

    • ./gradlew detektApply - check and fix code using Detekt (if possible)

  • Tests

    • ./gradlew lib:test - run JUnit tests

    • ./gradlew lib:apiTest - run API tests

    • ./gradlew lib:integrationTest - run integrations tests

    • ./gradlew lib:konsistTest - run Konsist tests to test Konsist codebase 🤯😉

  • - test classes using custom Kotlin snippets (
    .kttxt
    ) to test the Konsist API
  • konsistTest - tests Konsist codebase consistency using konsist library

  • snippets - contains Kotlin code snippets, written as methods (tests without @Test annotation), so the tests are not executed. These snippets are used to generate documentation. The update-snippets.py script generates PR to update the snippets page

  • GitHub discussion
    contributor backlog is public
    #konsist-dev
    kotlinlang Slack workspace
    #konsist-dev
    ContributeOpportunity
    #igorwojda
    kotlinlang Slack workspace
    Konsist
    develop
    Pull Request
    develop
    develop
    main
    konsist-documentation
    Konsist-documentation
    main
    new Pull Request
    main
    GitHub Actions
    .github/workflow
    Spotless
    ktlint
    Mermaid
    Mermaid plugin
    gradle.properties
    snippets
    JVM Test Suite Plugin
    test-project
    konsist-documentation
    Visual Studio Code
    Snippets
    snippet source set
    konsist
    snippet source set
    konsist-documentation
    this directory
    Inject.kt

    Create The Scope

    Access the Kotlin files using Konsist API

    Scope represents a set of Kotlin files to be further queried, filtered (Declaration Filtering), and verified (Declaration Assertion).

    Scopes are an alternative for baseline file. Subsets of the codebase can be refactored to be aligned with Konsist tests e.g. code in the single module.

    Every scope contains a set of KoFile instances. Every KoFile instance contains the declarations (see Declaration) representing code entities present in the file e.g.:

    Konsist is built on top of . It wraps the Kotlin compiler parser and provides a simple API to access Kotlin code base declarations. Konsist tree mimics the Kotlin code structure:

    The scope can be created for an entire project, module, package, and Kotlin file.

    The scope is dynamically built based on the Kotlin files present in the project, enabling it to adapt seamlessly as the project evolves. For instance, when the scope is set to encapsulate a specific module, any additional file introduced to that module will be automatically incorporated into the scope. This ensures that the scope consistently offers thorough and current coverage.

    To execute Konsist tests, the Konsist dependency must be integrated into a module. Yet, by integrating Konsist into a single module (e.g. app module), Konsist can still access the entire project. The specific files evaluated are determined by the evolving scope that's been defined.

    Scope Creation

    Various methods can be used to obtain instances of the scope. This allows the definition of more granular Konsist tests e.g. tests covering only certain modules, source sets, packages, or folders.

    See .

    Project Scope

    The widest scope is the scope containing all Kotlin files present inside the project:

    To print a list of files within koScope use the koScope.print() method:

    To review the scope content in more detail see .

    Production Codebase

    The scopeFromProduction method allows the creation of a scope containing only a production code (equivalent to Konsist.scopeFromProject() - Konsist.scopeFromTest()):

    Contains:

    Test Codebase

    The scopeFromTest method allows the creation of a scope containing only a test code:

    Contains:

    Module Scope

    The scopeFromModule method allows the creation of more granular scopes based on the module name e.g. creating a scope containing all Kotlin files present in the app module:

    Contains:

    This approach may be helpful when refactoring existing project modules by module.

    Nested Module Scope

    A nested module is a module that exists within another module.

    The nested modules the feature is not complete. The community is reporting that this feature works, however, we still have to take a closer look, review expectations, and add tests. Consider this feature as experimental for now.

    Consider this feature module existing inside app module:

    To narrow the scope to feature module use:

    Source Set Scope

    The scopeFromSourceSet method argument allows the creation of more granular scopes based on the source set name e.g. create a scope containing all Kotlin files present in the test source set:

    Contains:

    Module and Source Set Scope

    To retrieve scope by using both module and source set use the scopeFromProject method with moduleName and sourceSetName arguments:

    Contains:

    Package Scope

    The sourceFromPackage method allows the creation of a scope containing code present in a given package e.g. com.usecase package:

    Contains:

    The double dots (..) syntax means zero or more packages. Check the page.

    Directory Scope

    The scopeFromDirectory method allows the creation of a scope containing code present in a given project folder e.g. domain directory:

    Contains:

    File Scope

    It is also possible to create scope from one or more file paths:

    We have added a new way of creating the scope from a list of files. This can help with certain development workflows e.g. runing Konsist Tests only on files modified in a given PR:

    Scope Slice

    For even more granular control you can use the KoScope.slice method to retrieve a scope containing a subset of files from the given scope:

    The KoScope can be printed to display a list of all files present in the scope. Here is an example:

    Scope Reuse

    Reuse Scope In Test Class

    To reuse scope across the test class define the scope in the companion object and access it from multiple tests:

    Reuse Scope In Test Source Set

    To reuse scope across the multiple test classes define the scope in the file and access it from multiple test classes:

    Here is the file structure representing the above snippet:

    Scope Composition

    Konsist scope supports , so scopes can be further combined together to create the desired scope, tailored to project needs. In this example scopes from myFeature1 module and myFeature2 module are combined together:

    Scope Subtraction

    Scope subtraction is also supported, so it is possible for example to exclude a part of a given module. Here scope is created from myFeature module and then the ..data.. package is excluded:

    Print Scope

    To print all files within the scope use the print() method:

    See .

    Access Specific Declarations

    To access specific declaration types such as interfaces, classes, constructors, functions, etc. utilize the .

    ./gradlew publishToMavenLocal -Pkonsist.releaseTarget=local
    Mac: /Users/<user_name>/.m2/repository/com/lemonappdev/konsist
    Windows: C:\Users\<User_Name>\.m2\repository\com\lemonappdev\konsist
    Linux: /home/<User_Name>/.m2/repository/com/lemonappdev/konsist
    repositories {
        mavenLocal()
    }
    <repositories>
        <repository>
            <id>local</id>
            <url>file://${user.home}/.m2/repository</url>
        </repository>
    </repositories>
    Kotlin Compiler Psi
    Declaration
    Add Konsist Existing To Project (Baseline)
    Debug Konsist Test
    Package Wildcard
    Kotlin Operator overloading
    Debug Konsist Test
    Declaration Filtering
    Konsist.scopeFromProject() // All Kotlin files present in the project
    Konsist
        .scopeFromProject()
        .print()
    Konsist.scopeFromProduction()
    project/
    ├─ app/
    │  ├─ main/   <--- scope contains all production code files
    │  │  ├─ App.kt
    │  ├─ test/
    │  │  ├─ AppTest.kt
    ├─ core/
    │  ├─ main/   <--- scope contains all production code files
    │  │  ├─ Core.kt
    │  ├─ test/
    │  │  ├─ CoreTest.kt
    Konsist.scopeFromTest()
    project/
    ├─ app/
    │  ├─ main/
    │  │  ├─ App.kt
    │  ├─ test/   <--- scope contains all test code files
    │  │  ├─ AppTest.kt
    ├─ core/
    │  ├─ main/
    │  │  ├─ Core.kt
    │  ├─ test/   <--- scope contains all test code files
    │  │  ├─ CoreTest.kt
    Konsist.scopeFromModule("app")
    project/
    ├─ app/   <--- scope contains all files from the 'app' module
    │  ├─ main/
    │  │  ├─ App.kt
    │  ├─ test/
    │  │  ├─ AppTest.kt
    ├─ core/
    │  ├─ main/
    │  │  ├─ Core.kt
    │  ├─ test/
    │  │  ├─ CoreTest.kt
    val refactoredModule1Scope = Konsist.scopeFromModule("refactoredModule1")
    val refactoredModule1Scope = Konsist.scopeFromModule("refactoredModule2")
    
    val scope = refactoredModule1Scope + refactoredModule1Scop2
    
    scope
       .classes()
       ...
       .assertTrue { /*..*/ }
    project/
    ├─ app/   <--- scope contains all files from the 'app' module
    │  ├─ feature/
    │  │  ├─ Feature.kt
    Konsist.scopeFromModule("app/feature")
    Konsist.scopeFromSourceSet("test")
    project/
    ├─ app/
    │  ├─ main/
    │  │  ├─ App.kt
    │  ├─ test/   <--- scope contains all files the 'test' directory
    │  │  ├─ AppTest.kt
    ├─ core/
    │  ├─ main/
    │  │  ├─ Core.kt
    │  ├─ test/   <--- scope contains all files the 'test' directory
    │  │  ├─ CoreTest.kt
    Konsist.scopeFromProject(moduleName = "app", sourceSetName = "test)
    
    project/
    ├─ app/
    │  ├─ main/
    │  │  ├─ App.kt
    │  ├─ test/   <--- scope contains all files the 'test' directory
    │  │  ├─ AppTest.kt
    ├─ core/
    │  ├─ main/
    │  │  ├─ Core.kt
    │  ├─ test/
    │  │  ├─ CoreTest.kt
    Konsist.sourceFromPackage("com.usecase..")
    project/
    ├─ app/
    │  ├─ main/
    │  │  ├─ com/
    │  │  │  ├─ usecase/
    │  │  │  │  ├─ UseCase.kt <--- scope contains files present from 'com.usecase' package kon
    │  ├─ test/
    │  │  ├─ com/
    │  │  │  ├─ usecase/
    │  │  │  │  ├─ UseCaseTest.kt <--- scope contains files present from 'com.usecase' package
    val myScope = Konsist.scopeFromDirectory("app/domain")
    project/
    ├─ app/
    │  ├─ main/
    │  │  ├─ com/
    │  │  │  ├─ domain/  <--- scope contains files present in 'domain' folder
    val myScope = Konsist.scopeFromFile("app/main/domain/UseCase.kt")
    val filePaths = listOf("/domain/UseCase1.kt", "/domain/UseCase2.kt")
    val myScope = Konsist.scopeFromFile(filePaths)
    // scope containing all files in the 'test' folder
    koScope.slice { it.relativePath.contains("/test/") }
    
    // scope containing all files in 'com.domain.usecase' package
    koScope.slice { it.hasImport("com.domain.usecase") }
    
    // scope containing all files in 'usecase' package and its sub-packages
    koScope.slice { it.hasImport("usecase..") }
    // Test.kt
    class DataTest {
        @Test
        fun `test 1`() {
            classesScope
                .assertTrue { // .. } 
        }
    
        fun `test 2`() {
            classesScope
                .assertTrue { // .. } 
        }
        
        companion object {
            // Create a new KoScope once for all tests
            private val classesScope = Konsist
                .scopeFromProject()
                .classes()
        }
    }
    // Scope.kt is "test" source set
    val projectScope = Konsist.scopeFromProject() // Create a new KoScope
    
    // AppTest.kt
    class AppKonsistTest {
        @Test
        fun `test 1`() {
            projectScope
                .objects()
                .assertTrue { // .. }
        }
    }
    
    // DataTest.kt
    class CoreKonsistTest {
        @Test
        fun `test 1`() {
            projectScope
                .classes()
                .assertTrue { // .. }
        }
    
        fun `test 2`() {
            projectScope
                .interfaces()
                .assertTrue { // .. }
        }
    }
    project/
    ├─ app/
    │  ├─ test/
    │  │  ├─ app
    │  │     ├─ AppKonsistTest.kt
    │  │  ├─ core
    │  │     ├─ CoreKonsistTest.kt
    │  │  ├─ Scope.kt   <--- Instance of the KoScope used in both DataTest and AppTest classes.
    val featureModule1Scope = Konsist.scopeFromModule("myFeature1")
    val featureModule2Scope = Konsist.scopeFromModule("myFeature2")
    
    val refactoredModules = featureModule1Scope + featureModule2Scope
    
    refactoredModules
        .classes()
        ...
        .assertTrue { ... }
    val moduleScope = Konsist.scopeFromModule("myFeature")
    val dataLayerScope = Konsist.scopeFromPackage("..data..")
    
    val moduleSubsetScope = moduleScope - dataLayerScope
    
    moduleSubsetScope
        .classes()
        ...
        .assertTrue { ... }
    koScope.print()