Skip to content

1.1.0

Latest

Choose a tag to compare

@arnaudgiuliani arnaudgiuliani released this 29 Jul 14:13
5b3ccb3

A compile-safety architecture release: per-module validation is removed entirely in favor
of a single, authoritative full-graph check at each Koin entry point. This closes a real,
measured false-positive class, at the cost of leaf modules with no entry point of their own now
getting no compile-time safety diagnostics until something assembles a real graph around them.
Also ships incremental-compilation freshness hardening, allWarningsAsErrors compatibility, and a
collision-safe hint-file-naming scheme.

⚠️ Behavior change — per-module validation removed, full-graph validation only (#32, #51)

Why. A module validated in isolation cannot know how it will be wired into a larger app. This
stopped being theoretical: :core:notifications in a real playground app genuinely false-positived
on a dependency (PeerService) that a peer module provides — with no Gradle edge between the two,
the two are only unified downstream at the app's entry point. Per-module validation — checking a
module against its own definitions, its includes = [...], and its @Configuration siblings —
cannot see that far, and reported a hard KOIN-D001 for a dependency that resolves correctly once
the real app assembles both modules together. Rather than keep tuning the per-module oracle around
each new false-positive shape, per-module validation (and its "defer iff a provider exists
somewhere" oracle) is deleted outright. Full-graph validation — the check that runs at
startKoin/koinApplication/@KoinApplication — is now the sole compile-safety verifier.

What this means for you:

  • Rooted compiles (an app module with a real startKoin/koinApplication/@KoinApplication):
    more accurate. Genuine cross-module false positives like the peer-provider case above disappear;
    KOIN-D001 now always shows the real, assembled graph.
  • Leaf/library modules with no Koin entry point in their own compilation: KOIN-D001
    (missing dependency), KOIN-D004 (circular dependency), KOIN-D005/KOIN-D006 (parametersOf
    shape mismatches resolved via the graph), and KOIN-P001 (missing @PropertyValue) are now
    silent in that compilation — not because the module is safe, but because compile-time cannot
    know how it will be assembled downstream. The graph is still checked, correctly, at the real
    entry point once one exists in the compilation. This is disclosed via a default-visible
    (INFO-severity) message rather than failing silently; see logSeverity below to control its
    visibility.
  • KOIN-W002 (the old "deferred, no provider hint found anywhere" warning) is deleted — there
    is no more deferral machinery to warn about.
  • Circular-dependency detection (KOIN-D004) going silent for a leaf module is intentional, not a
    regression: detecting a cycle requires seeing the whole graph, and a same-module-only check was
    never a complete cycle detector even under the old per-module validation (it only ever saw
    local/sibling visibility).

Full account, including the design docs this reverses: docs/COMPILE_SAFETY_A3_PLAN.md
(superseded-banner) and docs/COMPILE_TIME_SAFETY.md.

🐛 Fixes

Orphaned @Module classes were silently treated as reachable (found during this release's own verification)

A plain @Module @ComponentScan(...) class with no @Configuration and not referenced by anyone's
includes = [...] was silently treated as part of the graph anyway, as long as the entry point used
a bare/default-labeled @KoinApplication/startKoin — the overwhelmingly common case. Its
@ComponentScan-discovered definitions (including cross-module ones) were folded into the resolved
graph and validated as satisfied, when the actual generated module tree never wired them in at all:
build green, runtime crash. Root cause: the entry-point module-discovery step accidentally called a
label-reader meant for the entry-point class's @KoinApplication(configurations=[...]) argument
against module classes, which never carry that annotation — so it always hit that reader's
"annotation absent" fallback (["default"]), making any @Module class match. This bug predates
1.1.0 (traced to 1.0.0-GA1) but was masked by the old per-module validation, which used to
validate each such module in isolation too; removing it made this bug load-bearing. Fixed, with a
regression test
(entry_orphan_module_not_reachable_d001) proving an orphaned module without @Configuration or an
includes edge is now correctly excluded from the graph.

KOIN-D001 now names the real culprit module and source location

Missing-dependency errors now carry file:line for the failing definition and the actual owning
module's name (previously degraded to a generic app/root label once every KOIN-D001 funnels
through the one remaining full-graph check). Also fixed: attribution for FunctionDef-shaped definitions
used a bare simple name, which could collide across same-named modules in different packages — now
uses the fully-qualified name.

KOIN-D001 deduplication across multiple entry points

A module reachable from more than one startKoin/koinApplication/@KoinApplication in the same
compilation (common in test-apps: ~9 entry points is typical) previously re-validated and re-emitted
the same missing-dependency error once per entry point. Now deduplicated by (definition, missing
requirement), so a shared module with one real problem reports it exactly once.

D005/D006 (parametersOf shape checks) no longer require a Koin entry point

The parametersOf(...) argument-count/presence check is graph-independent — the expected slots come
from the target's own constructor, not from an assembled graph — so it now runs unconditionally
instead of being skipped whenever no entry point is present in the compilation, matching its actual
data dependency. KOIN-D002 (call-site resolution) correctly keeps requiring an assembled graph
and stays silent without one — the two diagnostics no longer share a gate they don't share a
dependency on.

Cross-module qualifier and typed-scope resolution verified under the new sole-verifier design

New regression coverage confirms full-graph validation matches @Named qualifiers and typed
@Scope(X::class) keys correctly across Gradle module boundaries, not just "some provider of this
type exists somewhere" — this matters more now that there's no per-module fallback to catch a wrong
match.

Known pre-existing limitation, found while writing this coverage (not new, not fixed this
release):
BindingRegistry.findProvider's scope-visibility check only matches a typed
@Scope(X::class); a named @Scope(name = "...") provider has no scopeClass and is treated
as visible everywhere regardless of name.

🔒 Incremental-compilation freshness

Removing per-module leaf-local checking made full-graph validation's own freshness across
incremental (IC) rebuilds load-bearing in a way it wasn't before — these changes close that gap:

  • strictSafety is now mandatory once an aggregator is auto-detected, not opt-in. Previously,
    an explicit strictSafety = false silently won over the plugin's own startKoin/
    koinApplication/@KoinApplication detection, letting an aggregator's compileKotlin stay
    cacheable/up-to-date even when the DI graph changed underneath it (lambda-body DSL edits and
    new @ComponentScan-covered files don't register as ABI changes IC can see). strictSafety = true
    still works everywhere; the new escape hatch for a genuine detector misfire (the marker appears
    only in a comment/string, not a real entry point) is strictSafetyForceOff = true — a separate,
    explicit acknowledgement from a plain false.
  • Extended IC tracker linking: KoinDSLTransformer's 5 DSL definition call sites now register
    with ExpectActualTracker (alongside the existing LookupTracker calls), matching the pairing
    KoinAnnotationProcessor/KoinStartTransformer already had — closes another source of stale
    incremental state around DSL hint files.
  • A theorized @ComponentScan new-file freshness gap did not reproduce: adding a new
    @Singleton/@Factory class to a scanned package is itself a source-set input change, which
    Gradle already invalidates the owning module's compileKotlin task for, independent of anything
    Koin-specific — verified live on a real playground app. No plugin-side fix was needed here.
  • Known limitation, unchanged by this release: a module going completely empty (its last
    includes() or its last local definition removed, with nothing replacing it) is not detected
    incrementally without a full clean + --no-build-cache. This is a K2-internals residual (a
    keep-alive hint's signature not being re-resolved within one IC session), not a missing source
    edge — see playground-apps/README.md's "Known limitation" note.

🔇 allWarningsAsErrors / -Werror compatibility (#73)

Informational plugin output (userLogs/debugLogs messages, the @Monitor-tracing-enabled
summary) was emitted at WARNING severity unconditionally, which fails a build compiled with
allWarningsAsErrors even though none of it is a real diagnostic.

  • New logSeverity option ("warning" default, or "info") covers all of the above.
  • New, separate versionCheckSeverity option covers only the Kotlin-version-compatibility
    warning ("you're on an unverified Kotlin version") — kept independent because muting informational
    noise shouldn't also silence a real compiler-compatibility risk; set it to "info" only after
    assessing that risk yourself.
  • Real diagnostics (KOIN-Dxxx/KOIN-Wxxx/etc.) are unaffected by either setting — they always
    report at their own severity.
koinCompiler {
    logSeverity = "info"           // downgrade informational output, default "warning"
    versionCheckSeverity = "info"  // downgrade the version-compatibility check, default "warning"
}

🧷 Hint-file collision safety (#75)

Five internal call sites that generate synthetic hint file names for cross-module discovery used
unbounded-length, collision-prone name sanitization (e.g. p.q_r.mod and p.q.r_mod both
flattening to p_q_r_mod). All five now go through one shared utility: a bounded, readable prefix
plus a 64-bit hash suffix computed over the untruncated input, so truncation itself can never cause
a collision — this is a plain naming fix, not a diagnostic; file names have no external
reconstructors, so this changes freely.

Separately, the frozen, cross-version-reconstructed function-name encoder
(flattenFqNameForHint) stays unchanged (other Kotlin modules built with an older plugin version
reconstruct it, so it can't just be swapped for a hash) — but two distinct DSL module ids can still
flatten to the same identifier through it. New diagnostic KOIN-D008 hard-errors on that
specific collision when it happens within one compilation (e.g. two zero-parameter keep-alive hints
sharing a signature, a real KLIB SignatureClashDetector failure mode) — there is no legitimate
scenario where two distinct modules should collide, so there's no opt-out. Detecting the same
collision across Gradle modules is a known, explicitly deferred gap (would need to run at the
entry-point aggregator over already-decoded ids) — documented, not silent.

✅ Compatibility

  • Koin: 4.2.0+
  • Kotlin: verified range 2.3.0–2.3.10 (see CLAUDE.md for the version-gate policy)

📦 Install

plugins {
    id("io.insert-koin.compiler.plugin") version "1.1.0"
}

Full changelog: 1.0.2...1.1.0