- 静态分析
- 代码质量
【免费下载链接】pmd
An extensible multilanguage static code analyzer.
本文围绕 PMD 官方架构决策记录ADR 2 - Policy on the use of Kotlin for development(位于 docs/pages/pmd/projectdocs/decisions/adr-2.md)展开,系统解读 PMD 团队在引入 Kotlin 时制定的边界与规则,并结合当前仓库中的源码、Maven 配置与测试代码,验证这些决策的实际落地情况。读完本文,你将理解:PMD 为什么对 Kotlin"开放但设限"、六条具体决策规则各自的动机、pmd-core保持纯 Java 的兼容性含义,以及语言模块在混合语言与纯 Kotlin 两种路径之间如何取舍。
背景:PMD 用 ADR 记录架构决策
PMD 是一个有着二十多年历史的多语言静态代码分析工具(详见 README.md),长期以来大量架构决策仅存在于开发者脑中。为此项目引入了"架构决策记录(Architecture Decision Records,ADR)"机制——即 ADR 1:所有重大决策以统一模板(Context / Decision / Status / Consequences)写入仓库,随文档站点发布,并通过 Pull Request 讨论、将状态从 "Proposed" 提升到 "Accepted"。所有已接受的 ADR 会汇总在 docs/pages/pmd/projectdocs/decisions.md 的索引页中。
ADR 2 正是在这一机制下于 2022 年 7 月提出、同年 9 月被接受(见其 Change History 章节),它专门回答一个问题:Kotlin 应该在 PMD 代码库中用到什么程度?
Context:为什么此前对 Kotlin 保持谨慎
ADR 2 的 Context 部分记录了决策时的三个现实约束:
- 现状是"仅测试用 Kotlin":当时 Kotlin 只出现在部分单元测试中,例如
pmd-lang-test模块提供了一批基于 Kotlin 的测试基类(这点在 pmd-lang-test/pom.xml 的模块描述 "This module uses Kotlin" 中可得到印证)。 - JetBrains IntelliJ IDEA 之外的开发支持不足:Eclipse 的 Kotlin 插件按用户评价不能正常工作;VS Code 虽有 Kotlin 插件但仅具备基础功能;而 gitpod.io、GitHub Codespaces 等在线 IDE 大多基于 VS Code。这意味着在 PMD 中引入 Kotlin,就等于接受"PMD 只能靠 IntelliJ IDEA 开发",这是一种事实上的供应商锁定(vendor lock-in)。
- 多语言混合的维护成本:在 Java 之外混入第二种语言会让维护与新人贡献门槛变高。不过文档也坦承,PMD 本身就是一个处理数十种语言的工具,这个理由的说服力有限。
与此同时,扩展 Kotlin 使用也有明确收益:可以吸引更多贡献者(Kotlin 开发者生态庞大),也有利于在项目中开发小型 DSL。这些利弊权衡直接塑造了后面的决策内容。
Decision:六条规则,界定 Kotlin 的边界
ADR 2 的核心决策是:总体上对在 PMD 中增加 Kotlin 的使用持开放态度,但为了把扩展限制在可维护范围内,制定了六条规则。
规则一:pmd-core必须保持纯 Java
The module
pmd-coreshould stay in plain Java.
pmd-core是所有语言模块共享的核心 API(解析、规则、报告等基础设施)。保持纯 Java 的首要目的是保持二进制兼容性——当源码变更时,纯 Java 的核心可以最大程度避免兼容性破坏。文档同时解释了当时的背景:PMD 目前所有模块是同步发布的,所以兼容性压力暂时不大;但未来各语言模块变更频率差异大,若模块间能保持兼容,就没有必要每次都整体发布——届时pmd-core的纯 Java 属性将更有价值。
仓库实证:在根 pom.xml 中,Kotlin 编译插件(kotlin-maven-plugin)与 Kotlin 标准库依赖只在需要使用 Kotlin 的模块中生效;而 pmd-core/pom.xml 的插件与依赖清单里没有任何 Kotlin 相关条目。从当前源码树看,pmd-core的src/main与src/test下也确实不存在任何.kt文件——"纯 Java"并非口号,而是被构建配置与源码现状共同维持的硬约束。
规则二:测试代码可以使用 Kotlin
For (unit) testing, Kotlin can be used in
pmd-coreand in the language modules.
单元测试被明确列为可自由使用 Kotlin 的领域,覆盖pmd-core与各语言模块。测试框架层面则给出了现状与目标:pmd-test目前还未使用 Kotlin,而pmd-lang-test已经使用。
仓库实证:pmd-lang-test正是 ADR 落地最典型的模块。其 src/main/kotlin 目录下提供了大量 Kotlin 测试基座,例如:
- BaseTextComparisonTest.kt:把源码文件 dump 结果与基准文件比较的通用测试基类,其子类包括
CpdTextComparisonTest与BaseTreeDumpTest; - BaseParsingHelper.kt:语言无关的解析器工具基类,封装了
parse、parseResource、executeRule等测试常用入口; - BaseTreeDumpTest.kt:AST 结构 dump 对比测试基类。
此外 pmd-lang-test/pom.xml 以compile作用域引入Kotest(kotest-assertions-core-jvm、kotest-property-jvm、kotest-runner-junit5-jvm),让这些 Kotlin 测试基类连同断言库一起自动对使用者可用。
规则三:语言模块可以按需选择实现语言
from now on, we allow to have the individual language modules be implemented in different languages when it makes sense.
各语言模块获得自主权:可以继续用纯 Java(像pmd-java现在这样),也可以引入 Kotlin。这一条与 ADR 2 开头"引入 Kotlin 可以增加贡献、便于开发 DSL"的判断直接呼应——对于某些语法树构建、模式匹配密集的模块,Kotlin 的表达力能显著降低实现成本。
仓库实证:pmd-apex模块是"存量模块混入 Kotlin"的现行案例。其生产代码 ApexTreeBuilder.kt(约 830 行,位于src/main/java目录下但以.kt为扩展名)承担将 Apex 解析器产出转换为 PMD AST 的核心构建逻辑,并由 ApexParser.java 以new ApexTreeBuilder(task, ...)直接调用。与此同时,pmd-apex/pom.xml 也配置了kotlin-maven-plugin与 Kotlin 标准库依赖。也就是说,Java 与 Kotlin 在同一个模块、同一份生产代码中混合协作是 ADR 规则三允许并已经发生的现实。
规则四:混合语言不得破坏纯 Java 使用方式
When mixing languages (e.g. Java + Kotlin), we need to care that the modules can still be used with plain Java.
这是对下游用户的保护条款。以pmd-java为例,它向自定义规则作者提供了符号表(symbol table)、类型解析(type resolution)等 API;即便模块内部混入 Kotlin,也不能强迫用户必须用 Kotlin 才能写自定义规则——至少对已经存在、且用户可能已用 Java 写过规则的语言模块如此。换言之:Kotlin 是内部实现选项,不是外部使用门槛。
规则五:全新语言模块允许"纯 Kotlin"实现
It is also possible to write the entire language module in Kotlin only.
对于全新的语言模块,允许整模块用 Kotlin 实现,此时规则四中"混合语言"的兼容性难题自然消失(不存在历史 Java 规则)。这条规则为未来以 Kotlin 优先(Kotlin-first)方式接入新语言留出了空间。
仓库实证:虽然目前各存量语言模块仍以 Java 为主,但 PMD 已经拥有一个名为pmd-kotlin的模块——它负责分析 Kotlin 语言本身(基于 ANTLR4 语法 KotlinParser.g4,根节点KotlinFile、节点前缀Kt,参见 pmd-kotlin/pom.xml),说明 PMD 对 Kotlin 生态的重视;而"用 Kotlin 写新语言模块"的通道,则留给未来的新语言接入。
规则六:重构存量模块必须保持兼容,否则就是主版本变更
When refactoring an existing language module from Java only to introduce Kotlin, care needs to be taken to not make incompatible changes. If compatibility (binary or source) can't be maintained, then that would be a major version change.
如果要把一个纯 Java 的存量语言模块重构为引入 Kotlin,必须保证不产生不兼容变更;一旦二进制或源码兼容性无法维持,则该变更只能随主版本号(MAJOR)升级发布。这条规则与 ADR 3 确立的 API 演进原则(Semantic Versioning:MAJOR 用于不兼容变更;internal/impl包与@InternalApi、@Experimental注解界定非公开 API)形成呼应——Kotlin 引入的兼容性成本,最终由版本策略来兜底。
构建基础设施:根 pom.xml 中的 Kotlin 管线
上述规则能否落地,取决于构建系统是否配套。根 pom.xml 集中定义了 Kotlin 相关的统一配置:
| 配置项 | 值 | 含义 |
|---|---|---|
kotlin.version | 2.2.10 | Kotlin 编译器版本(见 pom.xml) |
kotlin.compiler.apiVersion/languageVersion | 1.9 | 语言特性 API 与语言版本兼容目标 |
kotlin.compiler.jvmTarget | ${maven.compiler.test.target} | 与测试目标 JVM 字节码版本对齐 |
kotest.version | 5.9.1 | Kotest 测试框架版本 |
dokka.version | 2.0.0 | Kotlin 文档生成插件版本 |
在插件管理段(pom.xml),kotlin-maven-plugin通过add-test-source目标把src/test/kotlin注册为测试源码目录,再依次执行kotlin-compile(同时编译src/main/kotlin与src/main/java)和kotlin-test-compile(编译src/test/kotlin与src/test/java);maven-compiler-plugin的默认 compile/testCompile 被禁用,改由 Java 编译执行点接管,从而保证 Kotlin 与 Java 在同一模块中协同编译。pmd-lang-test还通过 dokka-maven-plugin 生成 Kotlin 文档。
Consequences:收益与代价并存
ADR 2 明确承认这一决策是双刃剑:
收益面
- 更多 Kotlin 开发者可以参与贡献;
- 内部开发小型 DSL(领域专用语言)变得更容易;
- 打开了通向更多语言的可能性——文档明确提到,未来不限于 Kotlin,例如
pmd-scala模块使用 Scala 也是合理方向(当前仓库确实存在 pmd-scala-modules/pmd-scala-common 模块,其部分测试已用 Kotlin 编写,如 ScalaParserTests.kt)。
代价面
- 除 IntelliJ IDEA 之外的 IDE 在阅读 PMD 源码时会遇到困难,Eclipse 实际上将无法继续实用;
- 维护一个多语言(polyglot)代码库的复杂度更高。
测试侧的大规模实践:pmd-java 的 Kotlin 测试生态
规则二(测试可用 Kotlin)在pmd-java中得到了最大规模的实践。该模块在 src/test/kotlin 下积累了上百个 Kotlin 测试文件,覆盖 AST 节点测试、符号解析(symbols包)、类型系统(types包,含internal/infer下的类型推断测试)等领域。其中 KotlinTestingDsl.kt 是一个典型的"DSL 开发"成果:它基于 Kotest 与tree-matchers/tree-printers库(com.github.oowekyala.treeutils)构建了一套节点匹配 DSL——例如JavaVersion枚举支持J9..J11这种区间运算符、shouldMatchNode让断言以声明式写出,并统一约定"不变量断言"(javaImplicitAssertions,如isParenthesized必须与parenthesisDepth > 0一致)。这与 ADR 2 中"Kotlin 便于开发小型 DSL"的动机完全吻合。
需要再次强调边界:这些 Kotlin 测试全部位于测试源码树,pmd-java的生产代码仍是纯 Java,且 pmd-java/pom.xml 只为测试引入kotlin-maven-plugin与kotlin-stdlib、kotlin-test-junit——这正是规则四"混合语言不影响用户以纯 Java 使用模块"的典型体现。
小结:ADR 2 的实践价值
从 ADR 2 出发,结合仓库现状可以提炼出 PMD 的 Kotlin 落地路径:核心保持纯 Java 以守护兼容性底线;测试代码放开使用 Kotlin(pmd-lang-test、pmd-java 测试均以此构建测试 DSL);语言模块生产代码按需引入(pmd-apex 的 ApexTreeBuilder.kt 已是 Java/Kotlin 混编实例);未来新语言模块可走纯 Kotlin 路线;任何不兼容变更都必须以主版本升级为代价。这条路径既利用了 Kotlin 的表达力与生态吸引力,又通过版本策略与 API 原则(ADR 3)把兼容性风险控制在明确边界内,是大型多语言静态分析项目引入第二语言的参考范本。
状态与变更历史
- 状态:Accepted(已接受)
- 最近更新:2024 年 2 月(对应 7.0.0)
- 2022-09-30:状态由 Proposed 改为 Accepted
- 2022-07-28:提出初始版本
- 静态分析
- 代码质量
【免费下载链接】pmd
An extensible multilanguage static code analyzer.
相关推荐
使用 ADR 模板记录架构决策:Atlantis 项目的架构决策记录实践指南
使用 ADR 模板记录架构决策:Atlantis 项目的架构决策记录实践指南 导读 本文围绕 Atlantis 仓库中的 docs/adr/template.m
DevOpsCI/CD基础设施Volcano 与 Kubernetes 版本兼容矩阵:从 v1.6 到 HEAD 的完整版本适配指南
Volcano 与 Kubernetes 版本兼容矩阵:从 v1.6 到 HEAD 的完整版本适配指南 Volcano 作为运行在 Kubernetes 之上的
5分钟上手万能模组管理器 Reloaded-II:一篇文章讲透跨游戏装模组的全流程
5分钟上手万能模组管理器 Reloaded II:一篇文章讲透跨游戏装模组的全流程 你同时玩着《索尼克英雄》《索尼克滑板》和《血源诅咒》,每个游戏都想装高清纹理
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考