Skip to content

Import the type, never the namespace - #3122

Merged
StylianosGakis merged 2 commits into
chore/qualify-namespace-importsfrom
chore/namespace-import-ktlint
Sep 8, 2026
Merged

Import the type, never the namespace#3122
StylianosGakis merged 2 commits into
chore/qualify-namespace-importsfrom
chore/namespace-import-ktlint

Conversation

@StylianosGakis

@StylianosGakis StylianosGakis commented Sep 3, 2026

Copy link
Copy Markdown
Member

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.md and 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.Success and Duration.Companion.seconds pass. Res.string and Clock.System do not: string.FOO names nothing, and System.now() reads as java.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.FOO into string.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-ktlint module, RuleSetProviderV3 against ktlint 1.8.0, the version kotlinter 5.6.0 bundles. Wired into every module through kotlinter's ktlint configuration, so it runs on every source set. Verified by planting a violation in design-system-hedvig: it reports in commonMain and iosMain, 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_PACKAGES has to exclude Kotlin/Native interop packages by prefix, because platform.Foundation.systemLocale is shaped exactly like a member import. That costs nothing today: platform.* appears in 8 files, all nativeMain, 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-imports already owns them.

ktlintCheck reports zero namespace-import hits across every module and source set, and ./gradlew lint passes.

@StylianosGakis
StylianosGakis force-pushed the chore/namespace-import-ktlint branch 2 times, most recently from b3d6313 to 60317c8 Compare September 4, 2026 12:51
@StylianosGakis
StylianosGakis force-pushed the chore/namespace-import-ktlint branch from 60317c8 to 33042a1 Compare September 4, 2026 13:20
@StylianosGakis StylianosGakis changed the title Import the type, never the namespace Enforce the import rule with a ktlint rule Sep 4, 2026
@StylianosGakis
StylianosGakis changed the base branch from chore/lint-config-fixes to chore/qualify-namespace-imports September 4, 2026 13:40
@StylianosGakis
StylianosGakis force-pushed the chore/namespace-import-ktlint branch from 33042a1 to 94fe530 Compare September 4, 2026 13:48
@StylianosGakis StylianosGakis changed the title Enforce the import rule with a ktlint rule Import the type, never the namespace Sep 4, 2026
@StylianosGakis
StylianosGakis force-pushed the chore/namespace-import-ktlint branch from 94fe530 to 3996229 Compare September 4, 2026 14:26
@StylianosGakis
StylianosGakis force-pushed the chore/namespace-import-ktlint branch 2 times, most recently from 9187c33 to 3a87ab9 Compare September 4, 2026 16:57
@StylianosGakis
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
StylianosGakis requested a review from a team as a code owner September 7, 2026 14:54
@StylianosGakis
StylianosGakis force-pushed the chore/namespace-import-ktlint branch from 3a87ab9 to c706329 Compare September 7, 2026 15:00
@StylianosGakis
StylianosGakis merged commit e57b29a into develop Sep 8, 2026
4 checks passed
@StylianosGakis
StylianosGakis deleted the chore/namespace-import-ktlint branch September 8, 2026 09:51
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.

2 participants