KtLint 和 Detekt 的代码规范配置

KtLint 和 Detekt 的代码规范配置

KtLint 和 Detekt 的代码规范配置


KtLint 和 Detekt 的代码规范配置:一个"过度配置"项目的踩坑记录


上个月把项目从 AGP 7.4 迁到 8.2,Gradle 升到 8.5,顺手把 KtLint 从 0.49.1 更新到 1.1.1。本地 ./gradlew spotlessCheck 刚跑完,CI 上就炸了三百多条 trailing-comma-on-declaration-site 的错误。更麻烦的是,这些文件上周刚被另一个同事用 IDE 的 Reformat Code 扫过,而 IDE 当时还没开启 trailing comma 选项。结果就是一整个下午都在处理这种毫无业务价值的格式冲突。这次升级让我意识到,KtLint 和 Detekt 这两个工具虽然都是 Kotlin 生态的静态分析标配,但把它们配到"不添乱"的程度,远比文档上写的几行 Gradle 代码要复杂。


KtLint 的版本跳跃与 EditorConfig 的隐性契约


KtLint 在 2023 年从 0.x 直接跨入 1.0.0,这个版本切换带来的破坏性远超我的预期。之前 0.49.x 时代积累下来的 .editorconfig 配置,在新版本里有一半失效了。最典型的就是 ij_kotlin_allow_trailing_comma,这个 IntelliJ IDEA 的配置键在 KtLint 0.50.0 之前还能被读取,到了 1.0.0 之后,KtLint 完全转向了 Kotlin Coding Conventions 的官方定义,trailing comma 从"可选"变成了"推荐默认开启"。如果你的项目里有人用 Windows、有人用 macOS,而 .editorconfig 里没有显式写 ktlint_standard_trailing-comma-on-declaration-site = enabled,那么不同机器上格式化出来的结果就会不一致。


我现在用的集成方式是通过 Spotless 插件(https://github.com/diffplug/spotless)来封装 KtLint,版本锁在 6.25.0。这种方式比直接调用 KtLint 命令行或 ktlint-gradle 插件要稳一些,因为 Spotless 会帮你处理增量检查和跨语言格式化。但坑在于,Spotless 的 ratchetFrom 功能在配合 Git merge base 使用时,如果某个模块的 Kotlin 文件全是上古代码,第一次全量格式化产生的 diff 可能大到让 Code Review 直接瘫痪。我的建议是,在遗留项目上首次引入 KtLint 时,先别急着绑定到 check 任务,而是单独跑一次 spotlessApply,把格式化债务一次性还清,提交一个独立的 PR。否则后续每个功能分支都会夹杂着格式改动,blame 历史就彻底毁了。


KtLint 1.1.x 之后还有一个隐晦的变更:import ordering 规则对 java.__PLACEHOLDER_ITALIC_1__kotlin.__PLACEHOLDER_ITALIC_2__,java.__PLACEHOLDER_BOLD_0__,kotlin.**,^,强迫 IDEA 放弃自己的默认模板,向 KtLint 靠拢。这种细节官方文档不会强调,但足以浪费你两个小时去 diff 两个工具的 import 排序算法差异。


Detekt 不是 KtLint 的替代品:规则域的划分


很多人会在 KtLint 和 Detekt 之间做二选一,这从根本上就是错的。KtLint 解决的是"代码长什么样",Detekt(https://github.com/detekt/detekt)解决的是"代码结构有多糟"。我目前在用的是 Detekt 1.23.5,它的规则域覆盖复杂度、潜在缺陷、代码异味和格式化四大类。但真正该被重视的,是它那套复杂度规则:CyclomaticComplexMethod、CognitiveComplexMethod、LongParameterList、TooManyFunctions。


这里有一个很具体的坑。Detekt 默认的 LongParameterList 阈值是 6 个参数,对于 Android 开发来说这几乎是个不可能达到的标准。Compose 的 Composable 函数里,modifier、onClick、state 一传,很容易就突破 6 个。如果直接把 Detekt 默认配置丢进项目,第一天就会产出上千条警告,团队很快就会对静态分析产生免疫,直接把 Detekt 关掉。我的做法是把 LongParameterListfunctionThreshold 调到 8,constructorThreshold 调到 10,并且把 ignoreDefaultParameters 设为 true。这样既能拦截那种传了十几个参数的 God Function,又不会在日常 UI 代码里刷屏。


另一个容易误伤的是 MagicNumber 规则。Detekt 默认会报所有不是 0、1、2 的硬编码数字。这在普通 Kotlin 业务代码里是合理的,但在 Compose 里你写 Spacer(modifier = Modifier.height(16.dp)) 如果被报 MagicNumber,就很荒唐。Detekt 1.23.x 支持 ignoreAnnotated: ['Composable'] 这种配置,但文档示例不够明显,很多人不知道可以按注解过滤。更隐蔽的问题是,如果你把 MagicNumberignorePropertyDeclaration 打开,它会对 const val SCREEN_WIDTH = 1080 这种声明放行,但不会对 val width = 1080 放行,导致同一份代码里有些数字能写死、有些不能,规则边界非常模糊。我个人对这个规则的态度是:在纯业务模块里开着,在 UI 层直接 suppress,不要试图用一套规则覆盖所有层级。


Type Resolution:Detekt 最容易被忽略的开关


Detekt 最致命的陷阱是 Type Resolution。简单来说,Detekt 有两套规则:一套不需要类型信息,跑得快;一套需要完整的类型解析(比如 UnnecessarySafeCallUselessCallOnNotNull),但默认不开启。如果你只是配置了 detekt { ... } 然后跑 ./gradlew detekt,那么你实际上只跑了前一套规则,后一套规则被静默跳过了。


要开启 Type Resolution,你必须显式调用 detektMaindetektTest 任务,而不是默认的 detekt。在 Gradle 配置里,还需要确保 jvmTarget 与你的 Kotlin 编译目标一致。我在 AGP 8.2 + Kotlin 1.9.22 的项目里遇到过这样的报错:Detekt found [error] 和 JVM target compatibility between source sets。这不是 Detekt 的 bug,而是因为你没有给 Detekt 任务显式指定 Java toolchain。修复方式是在 tasks.withType<io.gitlab.arturbosch.detekt.Detekt>().configureEach 里设置 jvmTarget = "17",与项目的 kotlinOptions.jvmTarget 对齐。


还有一个关于自定义规则开发的细节。如果你想基于 Detekt 写内部规则,1.23.x 的 API 要求你的自定义规则模块依赖 detekt-api,而运行时需要把规则打包成 jar 并通过 detektPlugins 配置引入。这里有个版本锁定的坑:detekt-api 的版本必须和 detekt-gradle-plugin 的版本完全一致。我曾经把插件升到 1.23.5,但自定义规则模块还依赖 1.23.3,结果 Gradle 没报版本冲突,只是运行时规则直接没加载,没有任何日志提示。排查这种静默失败非常消耗耐心。


当 KtLint 遇上 Detekt Formatting:重复报告的处理


Detekt 官方提供了一个 detekt-formatting 规则集,它本质上是对 KtLint 规则的二次封装。如果你的项目里已经通过 Spotless 或 ktlint-gradle 集成了 KtLint,再开启 detekt-formatting,就会收到双倍的格式报错:KtLint 报一遍,Detekt 又报一遍。更糟的是,两者的默认规则集版本可能不一致。比如 Detekt 1.23.5 内置的 KtLint 版本是 1.0.1,而你通过 Spotless 显式指定的 KtLint 版本是 1.1.1,这时候同一个 trailing comma 问题,两个工具给出的修复建议可能完全不同。


我的建议是彻底解耦这两个工具的权责。在项目里只保留一种格式化检查入口:用 Spotless + KtLint 管所有"代码风格"问题,包括缩进、换行、import 排序、trailing comma。在 Detekt 里完全禁用 detekt-formatting,把 Detekt 的配置文件 detekt.ymlformatting: 这一节整个关掉。Detekt 只负责复杂度分析(complexity)、潜在 bug(potential-bugs)、和代码异味(code-smell)。这样你在 CI 里看到的报错来源是清晰的:看到 Spotless 报错就知道是格式问题,跑一下 spotlessApply 就能解决;看到 Detekt 报错就需要审视代码结构,可能是需要重构。


这种分工还有一个好处:KtLint 的修复是自动且确定性的,而 Detekt 的很多规则(比如 NestedBlockDepth)没有自动修复能力。如果混在一起,开发者会下意识觉得所有 Detekt 报错都能自动修,结果跑完 ./gradlew detektAutoCorrect 发现还有几百条复杂度警告没消,体验非常割裂。


Gradle 配置的细节:任务依赖与增量构建


在 AGP 8.0 以后,Google 开始推 Configuration Cache 和 Build Configuration Cache。Detekt 和 KtLint 对此的支持程度并不一样。KtLint 通过 Spotless 集成时,Spotless 6.25.0 对 Configuration Cache 的支持已经比较完整。但 Detekt 1.23.x 在开启 Configuration Cache 时有个已知问题:如果你在 detekt { baseline = file("detekt-baseline.xml") } 里配置了 baseline 文件,而 baseline 文件在并行任务中被修改,可能会导致缓存失效。Detekt 2.0.0

DocumentsContract 的树形文档访问,SAF 的局限 2026-08-03

评论区