Import the type, never the namespace - #3122
Merged
StylianosGakis merged 2 commits intoSep 8, 2026
Merged
Conversation
This was referenced Sep 3, 2026
StylianosGakis
force-pushed
the
chore/namespace-import-ktlint
branch
2 times, most recently
from
September 4, 2026 12:51
b3d6313 to
60317c8
Compare
StylianosGakis
force-pushed
the
chore/namespace-import-ktlint
branch
from
September 4, 2026 13:20
60317c8 to
33042a1
Compare
StylianosGakis
changed the base branch from
chore/lint-config-fixes
to
chore/qualify-namespace-imports
September 4, 2026 13:40
StylianosGakis
force-pushed
the
chore/namespace-import-ktlint
branch
from
September 4, 2026 13:48
33042a1 to
94fe530
Compare
StylianosGakis
force-pushed
the
chore/namespace-import-ktlint
branch
from
September 4, 2026 14:26
94fe530 to
3996229
Compare
StylianosGakis
force-pushed
the
chore/namespace-import-ktlint
branch
2 times, most recently
from
September 4, 2026 16:57
9187c33 to
3a87ab9
Compare
StylianosGakis
marked this pull request as ready for review
September 7, 2026 14:54
Import the type, never the namespace. An import may shorten a qualified reference only when the short name still says what it is to someone reading that line cold. Sealed subclasses and enum entries pass that test and stay allowed; around 1200 such imports already exist and they are the house style. Members reached through a receiver that carries the meaning do not: Res.string, Res.drawable and Clock.System are banned, since string.FOO and System.now() lose what Res.string.FOO and Clock.System.now() tell the reader. Also bans import-only changes to lines that are not otherwise being edited. PR #3100 carried 29 gratuitous new imports and ~60 rewritten call sites through a single screen refactor, which buried the real change under churn.
kotlinter runs ktlint over every source set, so a custom ruleset enforces
the import rule everywhere: androidMain, commonMain, iosMain and plain JVM
modules alike.
Android Lint would be the more precise tool, since it resolves types, but it
cannot reach KMP modules at all, and 19 of the violations just cleaned up
live in one. 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.
Having no type resolution means 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.
StylianosGakis
force-pushed
the
chore/namespace-import-ktlint
branch
from
September 7, 2026 15:00
3a87ab9 to
c706329
Compare
panasetskaya
approved these changes
Sep 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A structured way to catch these #3130 problems, by enforcing the import rule through a ktlint rule.
🤖 AI description:
States the import rule in
CLAUDE.mdand enforces it with a ktlint rule. #3130 below has already fixed every existing violation, so the tree is clean when this lands.The rule. Import the type, never the namespace. An import may shorten a qualified reference only when the short name still says what it is to someone reading that line cold.
HomeUiState.SuccessandDuration.Companion.secondspass.Res.stringandClock.Systemdo not:string.FOOnames nothing, andSystem.now()reads asjava.lang.System. The rule also bans import-only changes to lines you are not otherwise editing.Why it needs writing down. PR #3100 was one screen refactor in one file, and most of its +250/-139 was not the refactor: 29 new imports and around 60 rewritten call sites shortening
Res.string.FOOintostring.FOO. The real change was buried under the churn, and the shortened forms read worse than what they replaced.The check. A ktlint custom ruleset in the new
hedvig-ktlintmodule,RuleSetProviderV3against ktlint 1.8.0, the version kotlinter 5.6.0 bundles. Wired into every module through kotlinter'sktlintconfiguration, so it runs on every source set. Verified by planting a violation indesign-system-hedvig: it reports incommonMainandiosMain, not only on Android and JVM modules.Why ktlint rather than Android Lint. Android Lint would resolve the owner instead of inferring it from the shape of the import path, but it cannot see a KMP module at all, since AGP registers no lint task there (246751841), and the design system is where 19 of the violations lived. The commit message records the four workarounds tried before giving up on it. #3123 added Lint as a second mechanism and was closed as not worth maintaining the same policy twice.
The limitation is precision, not reach. Without type resolution,
CAPITALIZED_PACKAGEShas to exclude Kotlin/Native interop packages by prefix, becauseplatform.Foundation.systemLocaleis shaped exactly like a member import. That costs nothing today:platform.*appears in 8 files, allnativeMain, and the repo has no other capitalized-package member imports. The rule's KDoc records what a permanent fix would need, an analyzer that both resolves types and runs on every source set.Star imports are deliberately not handled here; ktlint's own
standard:no-wildcard-importsalready owns them.ktlintCheckreports zeronamespace-importhits across every module and source set, and./gradlew lintpasses.