Skip to content

Cover KMP source sets with a ktlint namespace-import rule - #3108

Closed
StylianosGakis wants to merge 2 commits into
chore/namespace-import-rulefrom
chore/namespace-import-ktlint-rule
Closed

Cover KMP source sets with a ktlint namespace-import rule#3108
StylianosGakis wants to merge 2 commits into
chore/namespace-import-rulefrom
chore/namespace-import-ktlint-rule

Conversation

@StylianosGakis

@StylianosGakis StylianosGakis commented Aug 28, 2026

Copy link
Copy Markdown
Member

Stacked on #3110. Review that one first.

Why a second mechanism

The NamespaceImport Android Lint check from #3107 cannot reach KMP modules, so the 19 design system violations it just cleaned up are ones it could never have caught.

I tried to fix that properly before writing this:

Attempt Result
Apply standalone com.android.lint alongside the KMP plugin Works, catches commonMain. But every generateAndroidMainLintModel then depends on itself, and the build dies with a circular dependency as soon as two such modules depend on each other
checkDependencies = false Same cycle
android.experimental.lint.analysisPerComponent=false Same cycle
com.android.internal.lint Runnable lint, but JVM components only. A full run with a planted violation still in commonMain came back green

The root cause is upstream. AGP's com.android.kotlin.multiplatform.library contributes a lint {} DSL but registers no task that runs it, and it sits outside com.android.base, so plugin logic keying off the Android plugins does nothing there. Google issue 246751841 has tracked this since 2022, AGP release notes through 9.3.2 never mention KMP lint, and the KMP plugin docs don't mention lint in either the supported or the unsupported section. The consistent community answer is to use ktlint or detekt for common code.

What this does

kotlinter already runs ktlint over every source set, so hedvig-ktlint implements the same rule as a RuleSetProviderV3 custom ruleset against ktlint 1.8.0, the version kotlinter 5.6.0 bundles. It is wired into every module through kotlinter's ktlint dependency configuration.

The tradeoff

ktlint has no type resolution, so unlike the Lint check it cannot ask whether an owner is a class or a package. CAPITALIZED_PACKAGES carries that cost: Kotlin/Native interop packages are named after the framework they bind, so platform.Foundation.systemLocale is shaped exactly like a member import and has to be excluded by prefix.

Both checks are kept deliberately. On Android modules Lint remains the better signal, since it resolves types and reports inline in the IDE as you type. A violation there is reported twice, which only happens when someone is already wrong.

Verification

  • Fires on KMP commonMain: a planted import in Tooltip.kt reports [hedvig:namespace-import].
  • Zero false positives. A full lintKotlin sweep across all 1,374 files and every source set returns no namespace-import hits.
  • Edge cases pass: platform.Foundation.systemLocale in core-locale's nativeMain and every Duration.Companion.seconds import are clean.

Unrelated pre-existing failure worth knowing about

:feature-insurances and :feature-terminate-insurance currently fail lintKotlin on standard:indent, standard:wrapping and similar. I confirmed with a control run, changes stashed, that these are pre-existing and unrelated to this PR (38 identical errors). It does mean ktlintCheck is red on develop today.

The NamespaceImport Android Lint check cannot reach KMP modules, so the
19 design system violations it just cleaned up were ones it could never
have caught. AGP's com.android.kotlin.multiplatform.library registers a
lint {} DSL but no task that runs it, and it sits outside com.android.base,
so plugin logic keying off the Android plugins does nothing there. Applying
the standalone com.android.lint plugin alongside it does produce a working
lint that reads commonMain, but every generateAndroidMainLintModel then
depends on itself and the build fails with a circular dependency the moment
two such modules depend on each other. Neither checkDependencies=false nor
android.experimental.lint.analysisPerComponent=false avoids it, and
com.android.internal.lint only ever analyses the JVM components, which do
not include commonMain. Google issue 246751841 has tracked this since 2022.

kotlinter already runs ktlint over every source set, so the same rule lives
here as a custom ruleset and covers commonMain, iosMain and the rest.

ktlint has no type resolution, so unlike the Lint check this cannot ask
whether an owner is a class or a package. CAPITALIZED_PACKAGES carries that
cost: Kotlin/Native interop packages are named after the framework they
bind, so platform.Foundation.systemLocale is shaped exactly like a member
import and has to be excluded by prefix.

Both checks are kept. Lint stays the better signal on Android modules,
where it resolves types and reports inline in the IDE.
Renovate owns dependency freshness in this repo, so lint's GradleDependency
and NewerVersionAvailable notices are duplicate signal that nobody acts on.

They resolve against the shared root gradle/libs.versions.toml rather than
the module being linted, so they report identically regardless of which
module runs lint, and :hedvig-ktlint surfaced 77 of them for dependencies
it does not declare.
@StylianosGakis
StylianosGakis force-pushed the chore/namespace-import-rule branch from 237f0d4 to 45a0610 Compare September 2, 2026 12:56
@StylianosGakis
StylianosGakis force-pushed the chore/namespace-import-ktlint-rule branch from 2a392a0 to c315509 Compare September 2, 2026 12:56
@StylianosGakis

Copy link
Copy Markdown
Member Author

Superseded, no content lost. See the reasoning in the comment on #3110.

The ktlint rule from this PR now leads the stack in #3122, since it is the mechanism with full coverage. The lint.xml change that was sitting in here unmentioned is extracted into #3121, together with a convention plugin fix so a new module no longer fails lint by existing.

All three are drafts, rebased on current develop, and green.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant