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!
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 defaultKoScopeis built by walking the filesystem from the Gradle root project (detected viagradlew), so every module's CodeGuard test sees every module's Kotlin sources.Expected
./gradlew :module-a:testshould only verify rules against classes that belong tomodule-a(its own production sources).Actual
./gradlew :module-a:testfails citingFooService, a@Servicedefined inmodule-bwith a@Transactionalself-invocation.module-adoes not depend onmodule-b. Excerpt from the failure:servicePackageis similarly triggered with classes from both modules.Minimal repro
Two-module Gradle layout, neither module depends on the other:
module-b/src/main/kotlin/com/example/demo/b/FooService.kt:Both test classes (identical, just in different packages):
Running
./gradlew :module-a:test --tests "*CodeGuard*"fails with thenoSelfInvocationOfProxyMethodsmessage namingFooService, even though module-a has no dependency on module-b.Version
dev.protsenko:spring-boot-code-guard:1.0.9Hypothesis on root cause
Possibly caused by the default scope in
SpringBootRulesConfiguration:Konsist.scopeFromProduction()resolves the root viaPathProvider.rootProjectPath, which walks parent directories until it finds agradlew/gradlew.bat(seeGradleProjectRootDirResolverin Konsist). In a Gradle multi-module build this is the root project, not the subproject under test, soKoScopeCreatorCore.scopeFromProjectends 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 setscope = scopeFromModule("module-name")explicitly per test. The current default makesverify()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!