Skip to content

Commit 538004f

Browse files
Fix koin#2402: explicit @KoinApplication modules override discovered @configuration
Runtime Koin is last-wins, so the order of module loading determines which binding wins when multiple modules define the same type. Before: explicit `@KoinApplication(modules = [...])` loaded first, then auto-discovered `@Configuration` dependency modules. That meant a dependency's default implementation silently overrode the app's custom override, which is the opposite of typical intent ("the app customises the libraries, not the other way round"). After: discovered modules load first, explicit list last. Within each half, user-declared order is preserved — so `@KoinApplication(modules = [A, B, C])` loads A, then B, then C, with C winning over A and B as the declared order implies. Each entry's `@Module(includes = [...])` chain stays grouped with that entry. Dedupe is now against explicit, so a module re-listed in the explicit `modules = [...]` keeps its explicit position rather than being merged to the discovered half — user's explicit listing always wins positionally. Regression test (`testData/box/startkoin/explicit_modules_override_configuration.kt`): a @configuration core module provides DefaultFeature, the app's @KoinApplication(modules=[AppModule]) provides AppFeature as override, runtime asserts `get<Feature>().name() == "app-override"`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent cc9ee22 commit 538004f

5 files changed

Lines changed: 407 additions & 5 deletions

File tree

koin-compiler-plugin/src/org/koin/compiler/plugin/ir/KoinStartTransformer.kt

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -281,11 +281,24 @@ class KoinStartTransformer(
281281
// Discover modules filtered by configuration labels
282282
val discoveredModules = discoverConfigurationModules(configurationLabels)
283283

284-
// Combine explicit modules with auto-discovered @Configuration modules
285-
val allModules = (explicitModules + discoveredModules)
286-
.distinctBy { it.fqNameWhenAvailable }
287-
288-
return allModules
284+
// Koin is last-wins at runtime, so load order determines override precedence.
285+
//
286+
// Rule (#2402): auto-discovered @Configuration modules first, explicit
287+
// @KoinApplication(modules = [...]) last. That way an app module listing a
288+
// feature override wins over the discovered dependency, which matches the
289+
// typical intent of "the app customises the libraries, not the other way round".
290+
//
291+
// Within each half we preserve the user's declaration order so they can still
292+
// control fine-grained order via an explicit list (modules = [A, B, C] loads
293+
// A then B then C, and C wins among those three).
294+
//
295+
// Dedupe discovered AGAINST explicit (not the other way round) so a module the
296+
// user re-declares in the explicit list keeps its explicit position — and
297+
// therefore its explicit override priority.
298+
val explicitFqNames = explicitModules.mapNotNull { it.fqNameWhenAvailable }.toSet()
299+
val uniqueDiscovered = discoveredModules.filterNot { it.fqNameWhenAvailable in explicitFqNames }
300+
301+
return uniqueDiscovered + explicitModules
289302
}
290303

291304
/**

koin-compiler-plugin/test-gen/org/jetbrains/kotlin/compiler/plugin/template/runners/JvmBoxTestGenerated.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,12 @@ public void testConfiguration_discovery() {
462462
runTest("koin-compiler-plugin/testData/box/startkoin/configuration_discovery.kt");
463463
}
464464

465+
@Test
466+
@TestMetadata("explicit_modules_override_configuration.kt")
467+
public void testExplicit_modules_override_configuration() {
468+
runTest("koin-compiler-plugin/testData/box/startkoin/explicit_modules_override_configuration.kt");
469+
}
470+
465471
@Test
466472
@TestMetadata("koin_application.kt")
467473
public void testKoin_application() {

0 commit comments

Comments
 (0)