@@ -28,9 +28,33 @@ import java.util.concurrent.atomic.AtomicInteger
2828 * FIR extensions don't receive configuration directly, so we store it globally.
2929 */
3030object KoinPluginLogger {
31+ /* *
32+ * Per-compilation message collector storage.
33+ *
34+ * Was `@Volatile var messageCollector: MessageCollector`. Under K2's parallel-daemon mode
35+ * multiple compilations can share a daemon, and each one's `init()` writes through this
36+ * singleton. With a single volatile field, compilation B's `init()` could overwrite A's
37+ * collector while A was still mid-IR-generation — A's subsequent diagnostics would land in
38+ * B's output stream (wrong build's log).
39+ *
40+ * `InheritableThreadLocal` scopes the collector to the compilation's entry thread and any
41+ * threads it spawns (IR generation occasionally fans out). The volatile `fallbackCollector`
42+ * preserves the original behavior for callers reached without a per-thread context — bare
43+ * CLI, unit tests, and the legacy alias in [KoinPluginMessageCollector].
44+ *
45+ * Read order in [effectiveCollector]: thread-local first, fallback second.
46+ */
47+ @PublishedApi
48+ internal val threadCollector: InheritableThreadLocal <MessageCollector ?> =
49+ InheritableThreadLocal ()
50+
3151 @Volatile
3252 @PublishedApi
33- internal var messageCollector: MessageCollector = MessageCollector .NONE
53+ internal var fallbackCollector: MessageCollector = MessageCollector .NONE
54+
55+ @PublishedApi
56+ internal val effectiveCollector: MessageCollector
57+ get() = threadCollector.get() ? : fallbackCollector
3458
3559 @Volatile
3660 var userLogsEnabled: Boolean = false
@@ -86,15 +110,41 @@ object KoinPluginLogger {
86110 * trailing AI-assist CTA so it shares a per-file output bucket with the error,
87111 * preventing renderers (Gradle's K2 collector) from sorting the location-less CTA
88112 * ahead of file-anchored messages.
113+ *
114+ * Per-thread (InheritableThreadLocal) for the same daemon-parallel reason as
115+ * [threadCollector] — two compilations would otherwise stomp each other's anchor.
89116 */
90- @Volatile
91- private var lastDiagnosticLocation: CompilerMessageLocation ? = null
117+ private val lastDiagnosticLocation: InheritableThreadLocal <CompilerMessageLocation ?> =
118+ InheritableThreadLocal ()
119+
120+ /* *
121+ * Bind [collector] to the current thread's slot for the duration of one compilation phase
122+ * (typically `IrGenerationExtension.generate`). Use a try/finally with [unbindThreadCollector]
123+ * to ensure it's cleared even on exceptions.
124+ *
125+ * Needed because Gradle daemon worker pools can dispatch a compilation's IR phase on a
126+ * different thread than the one that called [init]; without a re-bind, the per-thread
127+ * collector slot would be empty and reads would fall through to [fallbackCollector]
128+ * (which a parallel compilation may have overwritten).
129+ */
130+ fun bindThreadCollector (collector : MessageCollector ) {
131+ threadCollector.set(collector)
132+ }
133+
134+ fun unbindThreadCollector () {
135+ threadCollector.remove()
136+ }
92137
93138 /* *
94139 * Initialize the logger with configuration from the compiler.
140+ *
141+ * Writes the collector to both the per-thread slot (so this compilation's diagnostics
142+ * stay scoped) and to the volatile fallback (for callers reached outside the compilation
143+ * thread group — primarily the legacy [KoinPluginMessageCollector] alias).
95144 */
96145 fun init (collector : MessageCollector , userLogs : Boolean , debugLogs : Boolean , unsafeDslChecks : Boolean = true, skipDefaultValues : Boolean = true, compileSafety : Boolean = true, aiAssist : Boolean = true, moduleId : String? = null, lookupTracker : LookupTracker ? = null) {
97- messageCollector = collector
146+ threadCollector.set(collector)
147+ fallbackCollector = collector
98148 userLogsEnabled = userLogs
99149 debugLogsEnabled = debugLogs
100150 unsafeDslChecksEnabled = unsafeDslChecks
@@ -104,7 +154,7 @@ object KoinPluginLogger {
104154 this .moduleId = moduleId?.takeIf { it.isNotBlank() }
105155 this .lookupTracker = lookupTracker
106156 highestDiagnosticSeverity.set(0 )
107- lastDiagnosticLocation = null
157+ lastDiagnosticLocation.remove()
108158 }
109159
110160 /* *
@@ -118,7 +168,7 @@ object KoinPluginLogger {
118168 */
119169 inline fun user (message : () -> String ) {
120170 if (userLogsEnabled) {
121- messageCollector .report(CompilerMessageSeverity .WARNING , " [Koin] ${message()} " )
171+ effectiveCollector .report(CompilerMessageSeverity .WARNING , " [Koin] ${message()} " )
122172 }
123173 }
124174
@@ -133,7 +183,7 @@ object KoinPluginLogger {
133183 */
134184 inline fun debug (message : () -> String ) {
135185 if (debugLogsEnabled) {
136- messageCollector .report(CompilerMessageSeverity .WARNING , " [Koin-Debug] ${message()} " )
186+ effectiveCollector .report(CompilerMessageSeverity .WARNING , " [Koin-Debug] ${message()} " )
137187 }
138188 }
139189
@@ -142,7 +192,7 @@ object KoinPluginLogger {
142192 * Use for critical messages that should never be silenced (e.g., @Monitor without SDK).
143193 */
144194 fun warn (message : String ) {
145- messageCollector .report(CompilerMessageSeverity .WARNING , " [Koin] $message " )
195+ effectiveCollector .report(CompilerMessageSeverity .WARNING , " [Koin] $message " )
146196 }
147197
148198 /* *
@@ -153,7 +203,7 @@ object KoinPluginLogger {
153203 */
154204 inline fun userFir (message : () -> String ) {
155205 if (userLogsEnabled) {
156- messageCollector .report(CompilerMessageSeverity .WARNING , " [Koin-FIR] ${message()} " )
206+ effectiveCollector .report(CompilerMessageSeverity .WARNING , " [Koin-FIR] ${message()} " )
157207 }
158208 }
159209
@@ -165,7 +215,7 @@ object KoinPluginLogger {
165215 */
166216 inline fun debugFir (message : () -> String ) {
167217 if (debugLogsEnabled) {
168- messageCollector .report(CompilerMessageSeverity .WARNING , " [Koin-Debug-FIR] ${message()} " )
218+ effectiveCollector .report(CompilerMessageSeverity .WARNING , " [Koin-Debug-FIR] ${message()} " )
169219 }
170220 }
171221
@@ -174,7 +224,7 @@ object KoinPluginLogger {
174224 * This will cause compilation to fail.
175225 */
176226 fun error (message : String ) {
177- messageCollector .report(CompilerMessageSeverity .ERROR , " [Koin] $message " )
227+ effectiveCollector .report(CompilerMessageSeverity .ERROR , " [Koin] $message " )
178228 }
179229
180230 /* *
@@ -185,7 +235,7 @@ object KoinPluginLogger {
185235 val location = if (filePath != null ) {
186236 CompilerMessageLocation .create(filePath, line, column, null )
187237 } else null
188- messageCollector .report(CompilerMessageSeverity .ERROR , " [Koin] $message " , location)
238+ effectiveCollector .report(CompilerMessageSeverity .ERROR , " [Koin] $message " , location)
189239 }
190240
191241 /* *
@@ -205,8 +255,8 @@ object KoinPluginLogger {
205255 val location = if (filePath != null && line >= 0 ) {
206256 CompilerMessageLocation .create(filePath, line, column.coerceAtLeast(0 ), null )
207257 } else null
208- if (location != null ) lastDiagnosticLocation = location
209- messageCollector .report(severity, body, location)
258+ if (location != null ) lastDiagnosticLocation.set( location)
259+ effectiveCollector .report(severity, body, location)
210260 }
211261
212262 /* *
@@ -224,17 +274,17 @@ object KoinPluginLogger {
224274 * Called once per compilation by [org.koin.compiler.plugin.ir.KoinIrExtension] at
225275 * the tail of IR processing.
226276 */
227- fun flushAiAssistCta (collector : MessageCollector = messageCollector ) {
277+ fun flushAiAssistCta (collector : MessageCollector = effectiveCollector ) {
228278 val rank = highestDiagnosticSeverity.get()
229279 if (! aiAssistEnabled || rank == 0 ) return
230280 val severity = if (rank >= 2 ) CompilerMessageSeverity .ERROR else CompilerMessageSeverity .WARNING
231281 collector.report(
232282 severity,
233283 " [Koin] → Fix with AI: set up Kotzilla MCP at ${KoinPluginConstants .AI_ASSIST_CTA_URL } " ,
234- lastDiagnosticLocation,
284+ lastDiagnosticLocation.get() ,
235285 )
236286 highestDiagnosticSeverity.set(0 )
237- lastDiagnosticLocation = null
287+ lastDiagnosticLocation.remove()
238288 }
239289}
240290
0 commit comments