Rules Architecture turns the package-layout and architecture rules of our Kotlin projects into checks that fail the build. A rule that no tool checks decays, so instead of trusting a document, the checks read your sources and tell you which file breaks which rule and how to fix it.
- Per-file rules for detekt: 66 custom rules plus settings for detekt's own rules, with IDE support and
@Suppress. - Cross-file checks with Konsist: role import direction, use case shape, contract and implementation levels, DI placement, test mirroring and more.
- Unit structure checks: the unit graph (
apinever depends onimpl), one-sentence unit READMEs, no unit nested in another. - Gradle plugins: one plugin that applies detekt with the rules and runs the Konsist and structure checks
of every unit in
check, with a baseline for old code. - Multiplatform: the detekt rules read syntax only and the Konsist checks parse source directories, so both
work on
commonMainas well as on JVM sources.
Five artifacts, one version:
| Artifact | Use it for |
|---|---|
ru.astrainteractive.klibs.rules:gradle-plugin |
Gradle plugins that wire the three artifacts below into a build |
ru.astrainteractive.klibs.rules:detekt |
detekt plugin with the rules and their settings |
ru.astrainteractive.klibs.rules:konsist |
Konsist checks, run from the command line |
ru.astrainteractive.klibs.rules:structure |
structure checks, called from your build logic |
ru.astrainteractive.klibs.rules:core |
the shared vocabulary of the checks, pulled in by the others |
[versions]
klibs-rules = "<latest-version>"
[libraries]
klibs-rules-detekt = { module = "ru.astrainteractive.klibs.rules:detekt", version.ref = "klibs-rules" }
klibs-rules-konsist = { module = "ru.astrainteractive.klibs.rules:konsist", version.ref = "klibs-rules" }
klibs-rules-structure = { module = "ru.astrainteractive.klibs.rules:structure", version.ref = "klibs-rules" }Two plugins, both from ru.astrainteractive.klibs.rules:gradle-plugin, published to Maven Central. Add
mavenCentral() to the pluginManagement repositories, then apply a plugin to every unit. Apply the linter to the
root project too when using architectureGraphCheck:
plugins {
id("ru.astrainteractive.klibs.rules.gradle.linter") version "<version>"
}| Plugin id | Does |
|---|---|
ru.astrainteractive.klibs.rules.gradle.detekt |
applies detekt with a bundled detekt.yml, the ktlint wrapper and the compose rules |
ru.astrainteractive.klibs.rules.gradle.linter |
applies the detekt plugin and the architecture rules, and adds architectureCheck |
Detekt plugin.
- Applies
dev.detektand registers adetektFormattask with auto-correction on. - Writes the bundled
detekt.ymlto the root project'sbuild/detektonce for the whole build, and enables HTML reports, parallel execution andallRules. The project's own detekt config (config/detekt/detekt.ymlordetekt { config }) is read after the bundled one, so its settings win. - Resolves types against the Kotlin JVM target of the project, or the JVM that runs Gradle when there is none.
Set another target with
detektRules { jvmTarget.set("21") }. detektanddetektFormatread every.ktand.ktsfile of the project directory; the source-set tasks (detektMain,detektTest, …) read only their source set, so each is resolved against its own classpath. No task enters a directory that is a build or a checkout of its own (a Gradle build or settings file, or a.gitentry, such as a worktree under.claude/worktrees), norbuildandresources; sources generated into a project'sbuilddirectory (moko-resources) are skipped by the source-set tasks too.
Linter plugin. Apply it instead of the detekt plugin.
- Adds the architecture detekt rules and their
detekt-architecture.yml, listed afterdetekt.ymland before the project's own config, and passes the module's explicit API mode to thedetekttask. - Registers
architectureCheck, whichcheckdepends on: the Konsist checks of the unit's package layout, run in a separate JVM, and the structure checks of its README, nested units and project dependencies. Konsist findings accept@Suppress("RuleName")on a declaration or@file:Suppress("RuleName")on a whole file. - Registers
architectureBaseline, which writes the current violations toarchitecture-baseline.txt; the check accepts what the file lists for findings that cannot carry a Kotlin suppression. - Registers
architectureLint, which runs the whole linter and no tests:architectureCheckand every detekt task of the unit, the type-resolution ones (detektMain,detektTest,detektMainAndroid, …) included, but notdetektFormat; on the root project alsoarchitectureGraphCheck.checkruns onlydetektwithout type resolution, so run./gradlew architectureLintto see every finding. - On the root project, registers
architectureGraphCheck, which checks the complete unit graph for dependency cycles and for dependencies on an entry point, a unit detected or set asentry-point(anapplicationplugin, aninstancespath); rootcheckdepends on it. Units contribute their dependencies through an isolated-projects-compatible shared build service.
The unit kind decides which layout rules apply. It is detected by convention and can be set:
linter {
unitKind.set("library") // context, library, api, contract, entry-point or technology
rootPackage.set("ru.astrainteractive.aspekt.module.jail")
// The project's own words, added to those of the package-layout rule
entryPointWords.addAll("slot", "panel")
technologyWords.add("mongo")
pluralConcepts.add("goods")
// Supertypes of classes the platform constructs, added to Application, Activity, Service, Worker and the others
platformClasses.add("WatchFaceService")
// UI kinds whose parts are extensions of the kind's context: fun SlotContext.backSlot() in slot/BackSlot.kt
extensionKinds.add("slot")
}The technology words also come from the unit's build file: the last segment of each library's group and the first
word of its artifact (org.mongodb:mongodb-driver-kotlin-coroutine gives mongodb), also cut at its first
camel-case boundary (com.github.MilkBowl:VaultAPI gives vaultapi and vault), generic words such as core
and runtime aside. The Konsist checks and the detekt rules that judge technology packages (BareIdentifier,
KindNameInPackage, RunBlockingOutsideBoundary) get the same words; architectureDetektSettings writes them into
build/detekt/architecture-settings.yml, read before the project's own detekt config, together with whether
RunCatchingCancellation accepts the inline CancellationException check: only when mikro-core is not in the
project's version catalog. A convention plugin sets
unitKind for the units it builds, which maps each plugin to a row of the unit direction table.
| Detected as | When |
|---|---|
api, contract |
the last segment of the project path |
technology |
a technology name as the last segment of the project path (data/exposed, data/room, data/memory), or a platform name outside core and the entry points (gui/bukkit) |
entry-point |
the application or com.android.application plugin, or a path under :instances |
library |
Kotlin explicit API mode strict in a unit applying maven-publish |
context |
anything else |
The plugins use the rules of their own version, so a version of the plugin brings the same version of the
detekt, konsist and structure artifacts. The sections below are for builds that wire the artifacts by hand.
Add the rules to a project that applies detekt, together with the settings of detekt's own rules. Copy
detekt-architecture.yml into the project and
list it after your own configuration, because a later file overrides an earlier one:
dependencies {
detektPlugins("ru.astrainteractive.klibs.rules:detekt:<version>")
// the settings configure ktlint's ImportOrdering
detektPlugins("dev.detekt:detekt-rules-ktlint-wrapper:<detekt-version>")
}
detekt {
// the rules switch themselves on in the jar's config/config.yml, which detekt reads only on top of its defaults
buildUponDefaultConfig.set(true)
config.from(files("config/detekt/detekt.yml", "config/detekt/detekt-architecture.yml"))
}The rules run on ./gradlew detekt and accept @Suppress("RuleName").
RedundantVisibilityModifier must know the explicit API mode of a library unit, and detekt passes it only to
its per-source-set tasks. In a unit with explicitApi(), pass it to the plain detekt task too:
tasks.withType<dev.detekt.gradle.Detekt>().configureEach {
freeCompilerArgs.add("-Xexplicit-api=strict")
}For old code, detekt's own baseline accepts today's findings: ./gradlew detektBaseline writes
detekt-baseline.xml, which detekt then reads.
The Konsist checks run in their own JVM, because Konsist embeds the Kotlin compiler. Its command-line entry
point, ru.astrainteractive.klibs.rules.konsist.cli.KonsistCli, checks one unit and writes one violation per
line as rule<TAB>path<TAB>subject<TAB>message; it finishes normally either way, so the caller decides what
fails the build. A task in the unit's build.gradle.kts:
val konsist by configurations.creating
dependencies {
konsist("ru.astrainteractive.klibs.rules:konsist:<version>")
}
val konsistReport = layout.buildDirectory.file("reports/konsist.tsv")
val unitPath = path
val konsistCheck = tasks.register<JavaExec>("konsistCheck") {
val reportFile = konsistReport.get().asFile
classpath = konsist
mainClass.set("ru.astrainteractive.klibs.rules.konsist.cli.KonsistCli")
args(
"--project-dir", projectDir.absolutePath,
"--unit-kind", "context",
"--unit-path", unitPath,
"--main", file("src/main/kotlin").absolutePath,
"--test", file("src/test/kotlin").absolutePath,
"--report", reportFile.absolutePath,
)
doLast {
val violations = reportFile.readLines()
check(violations.isEmpty()) { violations.joinToString(separator = "\n") }
}
}
tasks.named("check") { dependsOn(konsistCheck) }| Option | Value |
|---|---|
--project-dir |
the unit's directory; report paths are relative to it |
--unit-kind |
context, library, api, contract, entry-point or technology, see below |
--unit-path |
the unit's Gradle path; finds the root package (:modules:event-bukkit → …event) |
--main |
a main source directory, optionally sourceSet=path; repeat for every source set |
--test |
a test source directory, optionally sourceSet=path; repeatable |
--report |
the file the violations are written to |
--root-package |
the unit's root package, when the unit path does not find it |
Suppress a rule in Kotlin instead of adding its finding to a baseline. Use a declaration annotation for one class, interface, function or property, including all declarations nested in it:
@Suppress("ContractName")
interface UserDaoUse a file annotation for every finding of that rule in the file, including import, package and file-shape rules:
@file:Suppress("KindShape", "PlatformImport")
package com.example.featureSuppression applies to the named rules only. architectureBaseline remains available for structure and Gradle
build findings, and for findings about missing files or directories that cannot carry an annotation.
Every unit has the shape <unit root>[/<concept>]/<kind>/<File>. The unit kind decides the exceptions:
| Kind | The unit |
|---|---|
context |
is a sub-context unit (command, event-bukkit, gui/bukkit) |
library |
is a published library or core |
api |
holds platform-free code other units call (gui/api); no file imports a platform type |
contract |
holds the wire types a server and its client share |
entry-point |
holds the platform main class, which may sit at the root, and RootModule |
technology |
is wholly one technology's implementation (data/exposed); its concepts hold implementations directly |
The structure checks need the project's dependencies, so they are a plain Kotlin library for build logic, such as a convention plugin. Describe the unit and run the checks:
val checker = StructureChecker(listOf(UnitDependency(), UnitReadme(), NestedUnit(), RootSources()))
val violations: List<StructureViolation> = checker.check(
BuildUnit(
path = project.path,
directory = project.projectDir,
isEntryPoint = false,
isRoot = project.path == ":",
projectDependencies = setOf(":economy:api")
)
)| Rule | Reports |
|---|---|
ImplicitItParameter |
the implicit lambda parameter it |
AnonymousTuple |
Pair and Triple used as a type, constructed by name, or made with to outside mapOf(…) |
DataClassDefaultValue |
a default value in a data class that is not @Serializable |
ConstructorDefaultDependency |
a constructor default that creates a dependency, such as = Foo() |
TestFunctionName |
a @Test not named GIVEN_<state>_WHEN_<action>_THEN_<outcome> |
CalleeBeforeCaller |
a function that calls a function declared below it in the same class or file |
DeclarationOrder |
properties, secondary constructors, functions, init blocks, companion: out of this order; a top-level PREVIEW_*/Preview* fixture may follow the composables; anything after the companion object, nested types included |
NamespaceObject |
an object that only groups functions, except a Compose <Component>Defaults |
LocalOrInnerClass |
a class inside a function body and an inner class |
TimeAsPrimitive |
a time-named value (createdAt, timeoutMs, CARD_IN_MS) typed as a number or a string, or inferred from such a literal |
KDocHtmlTag |
Javadoc HTML (<p>, <code>) in KDoc |
ImplementationName |
the Impl and Interface suffixes and the I prefix on an interface |
KindNameInPackage |
Service outside a service package, Model/Entity in a model with no technology word (exposed, room, ...) before it, Helper/Util/Manager names |
ExpressionBodyOnOneLine |
an expression-body function whose signature and body are not one line |
BooleanOperatorWrapping |
a line break on either side of && or || |
ArgumentPerLine |
a call or constructor call with three or more arguments, a trailing lambda included, whose arguments in parentheses are not each on its own line |
TryCatchCancellation |
a catch in suspend code that also catches CancellationException without rethrowing it first (detekt's SuspendFunSwallowedCancellation without its runCatching half, which rejects the documented .propagateCancellationException()) |
HardcodedDispatcher |
a dispatcher read from kotlinx.coroutines.Dispatchers (IO, Default, Unconfined) outside di, a provider's override property and tests, matched through the Analysis API; a dispatcher from any injected interface passes; replaces detekt's InjectDispatcher |
PropertyReadBeforeDeclaration |
a property read while the object is constructed (an initializer, a delegate, an init block) before its declaration; lambdas, functions and const val are not judged; replaces detekt's PropertyUsedBeforeDeclaration |
GuardIfBraces |
an if without else whose body is one return in braces, when it fits one line of 120 columns |
EventCollection |
one-off events collected in LaunchedEffect without withContext(Dispatchers.Main.immediate), or elsewhere without repeatOnLifecycle |
CompletableDeferredUse |
a CompletableDeferred created and awaited in one function by the same coroutine (a gate awaited in a launch/async is allowed), or held in a property as a Unit/Boolean readiness flag |
BridgeCleanup |
an awaitClose or invokeOnCancellation cleanup that assigns state declared outside its bridge (the callbackFlow/suspendCancellableCoroutine block) or calls launch |
ExplicitInvoke |
a call name(…) of a parameter, property or local value instead of name.invoke(…) |
FunctionVerb |
a function whose first word is get (but a view model's getState), map, make, do, handle or process |
PropertyAsFunction |
a function with no parameters that only returns a property of its class or file |
OneTypeFamilyPerFile |
a top-level function (a composable included) beside a top-level type, and a second class with behaviour in one file |
FileLength |
a file that passes 400 lines |
ConstantOutsideCompanion |
a const val or literal val in a class body outside its companion, a private object of constants beside a type |
ComposableFileType |
a class, interface or object in a file with a @Composable function, except a *Defaults object |
ComposableName |
a @Composable that emits UI and whose name does not end in Composable, except previews, the theme function and an override such as Render(); a …ScreenComposable in a file of another name; a theme function outside <Theme>Composable.kt |
ThemeObjectFunction |
a function inside an object of a theme package |
ComposeConstantCase |
a const val not UPPER_SNAKE or a val not PascalCase in a composable or theme file |
PreviewBlockLast |
a non-preview declaration after the first @Preview, a preview helper above the code of the file, a @Preview not named …Preview |
RememberInName |
a remember… function whose body calls no remember |
ActualFileSuffix |
a file with an actual declaration whose name has no platform suffix (Name.android.kt) |
MutableStateFlowEmit |
emit/tryEmit on a property declared or initialised as a MutableStateFlow |
ImpureUpdateLambda |
an assignment to an outer name inside the lambda of update, getAndUpdate or updateAndGet |
SingleJobLaunchMode |
a launch on a single-job scope without a SingleJobMode, an unguarded launch in an on… handler of a view model or component |
StateFlowTimeout |
StateFlow.first() inside withTimeout or withTimeoutOrNull |
ThrowInsideRunCatching |
a throw inside the lambda of runCatching, mapCatching or recoverCatching |
RunCatchingCancellation |
a runCatching in a suspend function or coroutine lambda not followed by .propagateCancellationException() or the inline onFailure check for CancellationException |
ResultConsumption |
a Result discarded as a statement without consuming its success or failure |
GetOrThrowInMain |
getOrThrow() outside the test sources |
BareIdentifier |
a property named id/…Id typed String, Int, Long or UUID outside a @Serializable class or technology package, a …Id class that is not a value class |
CompanionFactoryFunction |
a create… function in a companion object |
ClockSystemOutsideDi |
Clock.System outside a di package, the test sources and previews (@Preview…) |
RolelessClassName |
a …Validator or …Factory class (a Decompose fun interface Factory and a type named like the supertype it implements, object Factory : PageContext.Factory, are allowed), …WarmUp/…Migration/…ViewModel outside its warmup/migration/viewmodel package |
DiModuleLifecycle |
a …Module that declares enable, disable, reload, onEnable, onDisable or onReload, or that starts a warm-up, migration, listener, task, service or launch without a lifecycle property |
RootModuleConstruction |
a RootModule constructor call that constructs anything but a DI module (a class of a di package, found through the call's import) or a Lifecycle |
SystemTimeCall |
System.currentTimeMillis() and System.nanoTime() |
RunBlockingInTest |
runBlocking in a test source set |
RunTestResult |
a @Test that calls runTest without declaring TestResult and returning runTest { … } from a block body; = runTest { } is reported too |
RunBlockingOutsideBoundary |
runBlocking in the main sources outside a top-level main function and outside a technology package |
ThreadPrimitive |
Thread constructed or used through its static members in the main sources |
CallbackFlowShape |
a callbackFlow whose block does not end with awaitClose (it throws at run time), launch { send } in a callback of it (not in the producer block itself), a rendezvous buffer, a bridge shared with shareIn/stateIn not WhileSubscribed() |
ChannelFlowShape |
a channelFlow that registers a listener without ending in awaitClose, or launches no coroutine of its own (a flow { }, or a callbackFlow when it waits in awaitClose) |
SharedFlowEvent |
a SharedFlow property of a view model, a MutableSharedFlow without extraBufferCapacity fed through tryEmit |
CallbackDeclaration |
an on… parameter or property of a function type outside a @Composable and a Modifier factory, an …Listener interface, an …Callback interface other than a Decompose fun interface On…Callback; a top-level On…Callback output not nested in its component |
KDocFiller |
a KDoc filler phrase, a @return of "the result" or of the return type's own words, a @param whose words only repeat its name, a summary of more than four sentences |
LambdaSingleIfGuard |
a lambda whose whole body is one if without else, unless the if only throws |
OperatorBeforeTrailingCall |
arithmetic in parentheses before a call or property ((a * b).sp), a chain of plus/minus/times/div/rem calls |
TrivialLambdaReference |
a lambda that only passes its parameters to one function, or a { obj.foo() } callback, instead of a function reference; in coroutine code only when the callee resolves to a non-suspend function |
ComposableWhenBranch |
a when branch of a @Composable function that spans several lines, except a block of several statements |
NonComposableFileInComposable |
a file in a composable package with no @Composable function, outside test sources |
DurationOverloadCall |
.inWholeMilliseconds/.inWholeSeconds/.inWholeNanoseconds passed to delay, withTimeout, debounce and other calls with a Duration overload |
detekt-architecture.yml configures detekt's own rules: ClassOrdering off, replaced by DeclarationOrder,
which puts init blocks after the functions they call; ForbiddenComment for region
markers and separator comments; ForbiddenImport for kotlinx.datetime.Instant, kotlinx.datetime.Clock and java.util.Date, for java.time types and java.util.Calendar outside technology packages (ForbiddenImport/time), and for platform and ORM
types in model, Compose excepted, outside technology packages (exposed, room, ...); RedundantVisibilityModifier; WildcardImport with no exceptions; ktlint ImportOrdering.
DataClassContainsFunctions, with no conversion exception, and DataClassShouldBeImmutable: a data class
holds fields only. GlobalCoroutineUsage: no global scope. PackageNaming with a lowercase-only pattern.
MagicNumber accepts a number in a property declaration, where the Compose rule names a value.
ForbiddenImport also bans DI frameworks (Koin, Dagger, Anvil, Metro, kotlin-inject, Kodein) and
the callback and thread tools of the no-callbacks rule: suspendCoroutine, trySendBlocking, Timer, TimerTask,
Handler, thread; Executor/Executors outside core (ForbiddenImport/executor) and CompletableFuture
outside technology packages (ForbiddenImport/future), and Compose's painterResource and stringResource
outside util, where the asPainter() and asComposableString() extensions wrap them
(ForbiddenImport/resource). SuspendFunSwallowedCancellation and
SuspendFunInFinallySection are active.
| Check | Reports |
|---|---|
KindShape |
a file loose in the unit root or a concept package, a package below a kind package (theme included); below composable, a second level, a model/util/theme sub-package, or files next to sub-packages |
RootPackagePath |
a root package that does not end with the unit name from its path, outside the entry point and libraries; a core unit may end at core, and the last segment may be the singular of a plural unit name |
ThemeVisibility |
a theme object that is not internal in a feature unit, or hidden in the design-system unit core/ui |
AnonymousProjectObject |
an anonymous object : implementing a type of the project instead of a library type |
KindImportDirection |
an import against the first table of arch-context.md: model and contracts depend on model only, implementations on contracts, model, util and their own technology packages, entry points on use cases, contracts, model and util, one entry point on another only between UI kinds, a router's implementation on the UI it opens, a screen only from what hosts or opens it, nothing on di |
PlatformImport |
a platform or ORM import in model, api or usecase, or anywhere in an api unit; the model of a UI unit (a file outside model imports Compose) may import Compose |
UseCaseShape |
*UseCase outside usecase, other names in usecase, more than one entry, a use case taking another, a pass-through invoke that is one call on one of its properties |
ContractPlacement |
a plain interface outside api (a use case's in usecase and a DI module's in di aside), a state holder's interface (…Component, …ViewModel) outside component/viewmodel, api included, an implementation inside api, a network/local/remote package; a Compose scope (: ColumnScope) is not a contract |
ForbiddenPackageName |
a forbidden word in a package created inside the unit; the root package may keep its unit's word; table, entity and database outside a technology unit or package |
UtilShape |
a class or object in util, an extension function outside util, its receiver's file and entry-point kinds included, except a private one in a composable file; names util/<Subject>Ext.kt, the element type for a collection; extensions in a kind listed in extensionKinds (slot) are allowed |
EntryPointVisibility |
a public class in a kind the platform invokes (command, event, expansion, route, service, warmup, ...), outside a library unit |
ErrorPlacement |
an error type outside a model package |
DiModuleVisibility |
a public DI module that a file of its own unit constructs, outside a library |
DiPlacement |
a *Module outside a di package |
NestedDataClass |
a top-level data class used as a property type by exactly one class, except a sealed variant or a *State |
TestMirror |
a test in another package or source set than its owner |
FakePlacement |
a Fake* outside a fake package of the tests, or in the main sources outside a <feature>/fake unit, and a test fake of another unit's contract outside testFixtures, and a class other than a fake in a <feature>/fake unit |
InMemoryPlacement |
an InMemory* outside a memory package of the main sources |
SealedFamilyFile |
a class or object whose direct supertype is a sealed type declared in another file |
StateTypePlacement |
a <Screen>State declared in a UI kind (a state holder, a composable, a menu), nested in a type there or beside it, instead of its own file in model |
NestedEnum |
a top-level enum used as the property type of exactly one class |
UtilFileName |
a util file of one receiver's extensions not named <Receiver>Ext.kt, a file named Utils.kt, Helpers.kt, Extensions.kt and the like |
UtilFunctionAsExtension |
a util function whose one parameter is a model of the unit or of a unit it depends on, instead of an extension |
ImplementationVisibility |
a public class in a technology package or a technology unit's concept, outside a library, core, stub and memory |
UseCaseAbstraction |
an interface or abstract class named …UseCase |
AndroidNamespace |
an Android unit whose namespace differs from its root package |
ApplicationKDoc |
KDoc outside a published library, other than a contract's failure case (@return failure, @return null, @throws) or a reason outside the code (a link, an issue key, a bug, workaround or quirk) |
ContractName |
an api interface named …Api, …Dao, …Storage or …Krate, a …Repository extending a generic repository or Crud base |
CoreShape |
a …UseCase class or a …Repository interface in a core unit |
RootModulePlacement |
a RootModule outside the entry-point unit or its di package |
EntryPointShape |
a file of an entry-point unit outside its root and its di package; a class the platform constructs (by its direct supertype) is sent to the root package, any other file out of the unit |
ContractUnitShape |
in a contract unit, a class or interface that is not @Serializable, a function with a body outside an object |
ComposeKindWithoutCompose |
a composable or theme kind package in a unit that imports no Compose; the root package and concepts are not judged |
PlatformPackage |
a package named after a platform (android, ios, jvm, …) in a unit with several main source sets |
PluralPackageName |
a plural package name below the unit root, except an inherently plural concept (news, settings) |
DuplicateRepresentation |
a technology data class with the same constructor parameter names as a model data class of the unit or of a unit it depends on |
FieldDependencyConstruction |
a behaviour class of the unit, or a library class named for a dependency's role (HttpClient, …Repository, …Gateway, …Database, …DataSource), constructed outside di; a value holder (no supertype, no function body, no init block) is not a dependency |
TestOwnerBehaviour |
a <Owner>Test.kt whose owner is a data class without a body or an enum without functions |
| Check | Reports |
|---|---|
UnitDependency |
a project dependency outside the allowlist for the unit's kind in arch-context.md; every unit under core/ui counts as the design-system unit and may depend on the others, cycles left to UnitDependencyCycle |
UnitDependencyCycle |
a cycle in the complete project-unit graph (root Gradle plugin) |
PluginImplUnit |
an impl unit in a build that has a platform unit, a plugin (root Gradle plugin) |
UnitReadme |
a unit without a README.md of one sentence |
NestedUnit |
a unit inside the directory of another unit |
ContainerSources |
sources in a container directory above a unit (modules/gui/src above modules/gui/api) |
UnitPath |
a sub-context unit at the root of the build instead of a container such as modules |
PlatformSourceSetUnit |
a unit named after a Multiplatform target (android, ios, jvm, ...) instead of a source set |
RootSources |
sources in the root project |
EntryPointDependency |
a dependency on an entry-point unit (a path with the segment instances) |
TechnologyDependency |
a technology dependency forbidden by the unit direction table, including unrelated technology units |
ApiUnitDependency |
an api unit depending on a unit that is not api, core or a storage contracts unit (dao, contract) |
RootBuildSubprojects |
a subprojects { } or allprojects { } block in the root build file |
ProjectDirMatchesPath |
a unit whose directory differs from its Gradle path |
ApiConfiguration |
a directly declared api dependency that is not exported through a Multiplatform framework (Gradle plugin) |
PublishedLibraryExplicitApi |
a Kotlin unit applying maven-publish without strict explicit API mode (Gradle plugin) |
UnpublishedExplicitApi |
a Kotlin unit with explicit API mode that does not apply maven-publish (Gradle plugin) |
ApiUnitPlatformDependency |
an external platform dependency (Paper, Bukkit, Fabric, Forge, Android, androidx but Compose, annotation and collection) in an api unit (Gradle plugin) |
| Path | Holds |
|---|---|
rules-kotlin/detekt |
the detekt rules, their config/config.yml and detekt-architecture.yml |
rules-kotlin/gradle-plugin |
the detekt and linter Gradle plugins |
rules-kotlin/konsist |
the Konsist checks and their command-line entry point |
rules-kotlin/structure |
the structure checks |
rules-kotlin/self-check |
the isolated build that runs the real linter on every unit and the graph |
Build and test locally:
cd rules-kotlin
./gradlew check # tests, 100% line coverage, detekt and architecture self-checks
./gradlew publishToMavenLocal # ru.astrainteractive.klibs.rules:{detekt,gradle-plugin,konsist,structure}check publishes the current artifacts to Maven Local, copies the sources into self-check, and runs
architectureCheck in every unit plus architectureGraphCheck, with no architecture baselines.
A ⭐ on the repository costs nothing and helps just as much.