Skip to content

Commit 238743a

Browse files
Merge pull request #56 from InsertKoinIO/arnaud/ktz-wall2-wasm-null-source
Attach resolvable source to IR-generated hint files (wasmJs/JS KLIB)
2 parents 1dd4ec2 + 4ad5562 commit 238743a

10 files changed

Lines changed: 185 additions & 26 deletions

File tree

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

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -239,13 +239,6 @@ class CallSiteValidator(private val context: IrPluginContext) {
239239
.replaceFirstChar { it.lowercaseChar() }
240240
val fileName = "${modulePrefix}${sanitizedName}_callsite.kt"
241241

242-
val firFile = buildFile {
243-
moduleData = firModuleData
244-
origin = FirDeclarationOrigin.Synthetic.PluginFile
245-
packageDirective = buildPackageDirective { packageFqName = hintsPackage }
246-
name = fileName
247-
}
248-
249242
// Anchor the synthetic hint file on the call site's source file so the path stays
250243
// stable across incremental rebuilds (see issue #32). Fall back to the
251244
// alphabetically-first source file in the module — also stable, just less local.
@@ -254,6 +247,16 @@ class CallSiteValidator(private val context: IrPluginContext) {
254247
?: "/synthetic"
255248
val fakeNewPath = Path(basePath).parent.resolve(fileName)
256249

250+
val firFile = buildFile {
251+
moduleData = firModuleData
252+
origin = FirDeclarationOrigin.Synthetic.PluginFile
253+
packageDirective = buildPackageDirective { packageFqName = hintsPackage }
254+
name = fileName
255+
// KLIB metadata serialization (Native/JS/Wasm) requires a resolvable io
256+
// File per file; a null sourceFile fails the wasm/js serializer (KT-82395).
257+
sourceFile = syntheticHintSourceFile(fakeNewPath.absolutePathString())
258+
}
259+
257260
val hintFile = IrFileImpl(
258261
fileEntry = NaiveSourceBasedFileEntryImpl(fakeNewPath.absolutePathString()),
259262
packageFragmentDescriptor = EmptyPackageFragmentDescriptor(

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

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -223,13 +223,6 @@ class DslHintGenerator(private val context: IrPluginContext) {
223223
val prefix = HintFilePrefix.of(firModuleData.name.asString())
224224
val fileName = prefix + buildDslHintFileName(targetClassId, hintName)
225225

226-
val firFile = buildFile {
227-
moduleData = firModuleData
228-
origin = FirDeclarationOrigin.Synthetic.PluginFile
229-
packageDirective = buildPackageDirective { packageFqName = hintsPackage }
230-
name = fileName
231-
}
232-
233226
// Anchor the synthetic hint file on a stable source path from the current compile
234227
// unit (see issue #32). Priority:
235228
// 1. The DSL call's own source file — always a file in the current module, and
@@ -251,6 +244,16 @@ class DslHintGenerator(private val context: IrPluginContext) {
251244
?: "/synthetic"
252245
val fakeNewPath = Path(basePath).parent.resolve(fileName)
253246

247+
val firFile = buildFile {
248+
moduleData = firModuleData
249+
origin = FirDeclarationOrigin.Synthetic.PluginFile
250+
packageDirective = buildPackageDirective { packageFqName = hintsPackage }
251+
name = fileName
252+
// KLIB metadata serialization (Native/JS/Wasm) requires a resolvable io
253+
// File per file; a null sourceFile fails the wasm/js serializer (KT-82395).
254+
sourceFile = syntheticHintSourceFile(fakeNewPath.absolutePathString())
255+
}
256+
254257
val hintFile = IrFileImpl(
255258
fileEntry = NaiveSourceBasedFileEntryImpl(fakeNewPath.absolutePathString()),
256259
packageFragmentDescriptor = EmptyPackageFragmentDescriptor(

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

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -299,12 +299,6 @@ class InjectedParamHintGenerator(
299299
// for the same target type don't produce identical class names at dex merge time.
300300
val modulePrefix = HintFilePrefix.of(firModuleData.name.asString())
301301
val fileName = "${modulePrefix}${KoinPluginConstants.INJECTED_PARAMS_HINT_PREFIX}${flat}.kt"
302-
val firFile = buildFile {
303-
moduleData = firModuleData
304-
origin = FirDeclarationOrigin.Synthetic.PluginFile
305-
packageDirective = buildPackageDirective { packageFqName = hintsPackage }
306-
name = fileName
307-
}
308302
// Anchor the synthetic hint file on a stable path from the current compile unit
309303
// (see issue #32). Priority: DSL registration site → target class source → sorted
310304
// first file in module. Mirrors [DslHintGenerator.generateDslDefinitionHints].
@@ -321,6 +315,16 @@ class InjectedParamHintGenerator(
321315
?: "/synthetic"
322316
val fakeNewPath = Path(basePath).parent.resolve(fileName)
323317

318+
val firFile = buildFile {
319+
moduleData = firModuleData
320+
origin = FirDeclarationOrigin.Synthetic.PluginFile
321+
packageDirective = buildPackageDirective { packageFqName = hintsPackage }
322+
name = fileName
323+
// KLIB metadata serialization (Native/JS/Wasm) requires a resolvable io File
324+
// per file; a null sourceFile fails the wasm/js serializer (KT-82395).
325+
sourceFile = syntheticHintSourceFile(fakeNewPath.absolutePathString())
326+
}
327+
324328
val hintFile = IrFileImpl(
325329
fileEntry = NaiveSourceBasedFileEntryImpl(fakeNewPath.absolutePathString()),
326330
packageFragmentDescriptor = EmptyPackageFragmentDescriptor(

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

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1003,12 +1003,6 @@ class KoinAnnotationProcessor(
10031003
}
10041004

10051005
val batchFileName = "koin_hints_${sanitizedModuleId}.kt"
1006-
val firFile = buildFile {
1007-
moduleData = firModuleData
1008-
origin = FirDeclarationOrigin.Synthetic.PluginFile
1009-
packageDirective = buildPackageDirective { packageFqName = hintsPackage }
1010-
name = batchFileName
1011-
}
10121006

10131007
val sourceFileEntry = try {
10141008
moduleClass.irClass.fileEntry
@@ -1021,6 +1015,17 @@ class KoinAnnotationProcessor(
10211015
}
10221016

10231017
val fakeNewPath = Path(sourceFileEntry.name).parent.resolve(batchFileName)
1018+
val firFile = buildFile {
1019+
moduleData = firModuleData
1020+
origin = FirDeclarationOrigin.Synthetic.PluginFile
1021+
packageDirective = buildPackageDirective { packageFqName = hintsPackage }
1022+
name = batchFileName
1023+
// KLIB metadata serialization (Native/JS/Wasm) requires every file to
1024+
// resolve to an io File; a synthetic file with no sourceFile fails the
1025+
// wasm/js serializer with "No file found for source null" (KT-82395).
1026+
sourceFile = syntheticHintSourceFile(fakeNewPath.absolutePathString())
1027+
}
1028+
10241029
val hintFile = IrFileImpl(
10251030
fileEntry = NaiveSourceBasedFileEntryImpl(fakeNewPath.absolutePathString()),
10261031
packageFragmentDescriptor = EmptyPackageFragmentDescriptor(
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package org.koin.compiler.plugin.ir
2+
3+
import org.jetbrains.kotlin.KtIoFileSourceFile
4+
import org.jetbrains.kotlin.KtSourceFile
5+
import java.io.File
6+
7+
/**
8+
* Source file attached to the synthetic FIR files the plugin generates for hints.
9+
*
10+
* KLIB metadata serialization walks every file in the module and resolves it to an
11+
* io [File] (`serializeModuleIntoKlib` → `KtSourceFile.toIoFileOrNull()`). A synthetic
12+
* file with no `sourceFile` resolves to `null`, which the Native backend tolerates but
13+
* the JS/Wasm serializer rejects with `IllegalStateException: No file found for source
14+
* null` (KT-82395). Anchoring the file on a [KtIoFileSourceFile] — even a path that is
15+
* not a real on-disk file — gives the serializer a non-null io File and keeps wasm/js
16+
* builds working.
17+
*
18+
* [path] should be the hint file's own deterministic path (the same value used for the
19+
* IR `fileEntry`), so the FIR source and IR file entry agree.
20+
*/
21+
internal fun syntheticHintSourceFile(path: String): KtSourceFile = KtIoFileSourceFile(File(path))
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
build/
2+
.gradle/
3+
.kotlin/
4+
local.properties
5+
kotlin-js-store/
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# app-kmp-klib — KLIB target regression sample
2+
3+
A minimal Kotlin Multiplatform app exercising the **KLIB-serialized** targets
4+
(`wasmJs`, `iosArm64`) that the JVM box tests can't cover. It is the repro behind
5+
compiler **#40** (wasmJs) and **#44** (iOS).
6+
7+
`App.kt` declares the same `@InjectedParam` target collected by **two
8+
`@ComponentScan` modules** — the exact shape that emitted the `injectedparams_*`
9+
hint twice and broke KLIB serialization.
10+
11+
## Build
12+
13+
```bash
14+
# from the repo root: publish the plugin to mavenLocal first
15+
./install.sh
16+
17+
cd playground-apps/app-kmp-klib
18+
../../gradlew compileKotlinIosArm64 # Kotlin 2.3.20 (floor)
19+
../../gradlew compileKotlinIosArm64 -PkotlinVersion=2.4.0
20+
../../gradlew compileKotlinWasmJs
21+
../../gradlew compileKotlinWasmJs -PkotlinVersion=2.4.0
22+
```
23+
24+
## Expected results
25+
26+
| Target | Kotlin 2.3.20 | Kotlin 2.4.0 |
27+
|---|---|---|
28+
| `iosArm64` (annotations) |||
29+
| `wasmJs` (DSL) |||
30+
| `wasmJs` (annotations / `@ComponentScan`) |`KT-82395` ||
31+
32+
- **iOS** and **wasmJs-DSL** pass on both Kotlin versions — the duplicate-signature
33+
fix (single hint per target) and the IR-hint source-attribution fix.
34+
- **wasmJs + annotations on Kotlin 2.3.20** fails with
35+
`IllegalStateException: No file found for source null`. This is a **Kotlin 2.3.20
36+
compiler bug** ([KT-82395](https://youtrack.jetbrains.com/issue/KT-82395)) in the
37+
JS/Wasm KLIB metadata serializer: the FIR cross-module discovery hints are placed
38+
in a framework-created file with no resolvable source. The Native backend tolerates
39+
it; the 2.3.20 wasm/js backend does not. **Fixed by Kotlin 2.4.0** — use Kotlin
40+
2.4.0 for annotation-based wasmJs.
41+
42+
> Not wired as a CI gate: the wasmJs-annotation cell is Kotlin-version-dependent by
43+
> design (passes on 2.4.0, blocked by KT-82395 on 2.3.20).
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
plugins {
2+
kotlin("multiplatform")
3+
id("io.insert-koin.compiler.plugin")
4+
}
5+
6+
kotlin {
7+
jvmToolchain(17)
8+
9+
// wasmJs: a KLIB target (no Xcode needed) — serialization rejects duplicate
10+
// injectedparams_* signatures, the actual failure mode of compiler#40/#44.
11+
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
12+
wasmJs {
13+
nodejs()
14+
binaries.executable()
15+
}
16+
17+
// iOS device target — the failing target in compiler#44. KLIB-serialized like
18+
// wasmJs, so it exercises the same duplicate-signature path on the native backend.
19+
iosArm64()
20+
21+
sourceSets {
22+
val commonMain by getting {
23+
dependencies {
24+
implementation("io.insert-koin:koin-core:4.2.1")
25+
implementation("io.insert-koin:koin-annotations:4.2.1")
26+
}
27+
}
28+
}
29+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Multiplatform KLIB verification app — proves the injectedparams_* hint is emitted
2+
// once so KLIB serialization (wasmJs/native) succeeds. JVM tolerates duplicates;
3+
// this target is where compiler#40/#44 actually failed.
4+
pluginManagement {
5+
val kotlinVersion: String = (settings.extra.properties["kotlinVersion"] as? String) ?: "2.3.20"
6+
plugins {
7+
kotlin("multiplatform") version kotlinVersion
8+
id("io.insert-koin.compiler.plugin") version "1.0.1"
9+
}
10+
repositories {
11+
mavenLocal()
12+
mavenCentral()
13+
gradlePluginPortal()
14+
}
15+
}
16+
17+
dependencyResolutionManagement {
18+
repositories {
19+
mavenLocal()
20+
mavenCentral()
21+
}
22+
}
23+
24+
rootProject.name = "app-kmp-klib"
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package playground
2+
3+
import org.koin.core.annotation.ComponentScan
4+
import org.koin.core.annotation.Factory
5+
import org.koin.core.annotation.InjectedParam
6+
import org.koin.core.annotation.Module
7+
8+
// Same @InjectedParam target collected by two @ComponentScan modules — the case
9+
// that emitted the injectedparams_* hint twice and broke KLIB serialization
10+
// (compiler#40 wasmJs, #44 iOS). Compiling this to wasmJs is the real fix proof:
11+
// the KLIB serializer fails the build on duplicate signatures, passes on a single one.
12+
13+
@Factory
14+
class Greeter(@InjectedParam val name: String)
15+
16+
@Module
17+
@ComponentScan
18+
class FirstModule
19+
20+
@Module
21+
@ComponentScan
22+
class SecondModule

0 commit comments

Comments
 (0)