Skip to content

verify() scans beyond the module under test in multi-module Gradle builds #6

@yluom

Description

@yluom

Summary

In a multi-module Gradle build, springBootRules { all() }.verify() running inside one module reports violations from classes that belong to a sibling module with no compile/runtime dependency on the module under test. The default KoScope is built by walking the filesystem from the Gradle root project (detected via gradlew), so every module's CodeGuard test sees every module's Kotlin sources.

Expected

./gradlew :module-a:test should only verify rules against classes that belong to module-a (its own production sources).

Actual

./gradlew :module-a:test fails citing FooService, a @Service defined in module-b with a @Transactional self-invocation. module-a does not depend on module-b. Excerpt from the failure:

java.lang.AssertionError:
CodeGuard:noSelfInvocationOfProxyMethods: FooService.outer is annotated with @Transactional
  and invokes @Transactional method inner of the same class — Spring AOP proxy is bypassed
  on self-invocation, the inner annotation will be silently ignored.
    at dev.protsenko.codeguard.core.SpringBootRulesConfiguration.verify(SpringBootRulesConfiguration.kt:124)

servicePackage is similarly triggered with classes from both modules.

Minimal repro

Two-module Gradle layout, neither module depends on the other:

codeguard-repro/
├── settings.gradle.kts          // include("module-a", "module-b")
├── build.gradle.kts             // kotlin("jvm") + plugin.spring + spring-boot
├── module-a/
│   ├── build.gradle.kts         // testImplementation("dev.protsenko:spring-boot-code-guard:1.0.9")
│   └── src/
│       ├── main/kotlin/com/example/demo/a/BarService.kt  // clean @Service
│       └── test/kotlin/com/example/demo/a/SpringBootCodeGuardTest.kt
└── module-b/
    ├── build.gradle.kts         // same code-guard dep, no dep on module-a
    └── src/
        ├── main/kotlin/com/example/demo/b/FooService.kt  // self-invocation violation
        └── test/kotlin/com/example/demo/b/SpringBootCodeGuardTest.kt

module-b/src/main/kotlin/com/example/demo/b/FooService.kt:

package com.example.demo.b

import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
class FooService {
    @Transactional
    fun outer() { this.inner() }

    @Transactional
    fun inner() { /* ... */ }
}

Both test classes (identical, just in different packages):

class SpringBootCodeGuardTest : FunSpec({
    test("spring-boot-code-guard - all rules") {
        springBootRules { all() }.verify()
    }
})

Running ./gradlew :module-a:test --tests "*CodeGuard*" fails with the noSelfInvocationOfProxyMethods message naming FooService, even though module-a has no dependency on module-b.

Version

  • dev.protsenko:spring-boot-code-guard:1.0.9
  • Konsist (transitive)
  • Gradle 9.5, Kotlin 2.3.21, Spring Boot 4.0.5, JDK 25

Hypothesis on root cause

Possibly caused by the default scope in SpringBootRulesConfiguration:

var scope: KoScope = Konsist.scopeFromProduction()

Konsist.scopeFromProduction() resolves the root via PathProvider.rootProjectPath, which walks parent directories until it finds a gradlew/gradlew.bat (see GradleProjectRootDirResolver in Konsist). In a Gradle multi-module build this is the root project, not the subproject under test, so KoScopeCreatorCore.scopeFromProject ends up enumerating Kotlin files for every module on disk.

One option would be to default to something like Konsist.scopeFromProject(moduleName = <current>), or document that adopters should set scope = scopeFromModule("module-name") explicitly per test. The current default makes verify() in module A surface violations from module B even when A and B share no classpath, which is surprising and turns one module's bad code into every module's failing build.

Happy to PR a documentation note or a default change if helpful — thanks for the library!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions