PMD:源码 AST、类型规则与 CPD 门禁
PMD 的结果由规则集和类型环境共同决定
PMD 将源码解析为语言 AST,按 ruleset 执行易错、复杂度、设计和最佳实践规则;部分 Java 规则还依赖 auxclasspath 提供类型信息。CPD 则按 token 序列寻找复制粘贴片段,是同一发行版中的另一项能力,但不能用重复代码结果代替语义规则。
本文实验锁定 PMD 7.26.0。它是可复现基线,不是永久最新声明;升级前查看官方发行记录与迁移说明。默认 category、规则属性和废弃路径可能随版本变化,因此生产门禁必须显式列出规则。
| 输入 | 影响 | 失真方式 |
|---|---|---|
| 源码与语言版本 | 决定 AST 能否正确解析 | 新语法未支持时分析失败或漏报 |
| ruleset | 决定规则、属性、优先级与 excludes | 整类引用升级后悄悄增加规则 |
| auxclasspath | 为类型感知规则提供符号 | 缺依赖导致类型分辨率下降 |
| incremental cache | 跳过未变输入以节省时间 | 被误当作历史违规 baseline |
Maven 插件和 PMD 引擎分开锁定
Maven PMD Plugin 自带一个 PMD 版本。若项目要采用不同引擎,必须显式覆盖依赖并重新验证插件兼容性。下面同时运行 PMD 与 CPD,规则文件保存在仓库固定路径。
<properties>
<maven.pmd.plugin.version>3.28.0</maven.pmd.plugin.version>
<pmd.version>7.26.0</pmd.version>
</properties>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-pmd-plugin</artifactId>
<version>${maven.pmd.plugin.version}</version>
<dependencies>
<dependency>
<groupId>net.sourceforge.pmd</groupId>
<artifactId>pmd-java</artifactId>
<version>${pmd.version}</version>
</dependency>
</dependencies>
<executions>
<execution>
<id>pmd-main</id>
<phase>verify</phase>
<goals>
<goal>check</goal>
<goal>cpd-check</goal>
</goals>
<configuration>
<rulesets>
<ruleset>${maven.multiModuleProjectDirectory}/config/pmd/ruleset.xml</ruleset>
</rulesets>
<failOnViolation>true</failOnViolation>
<printFailingErrors>true</printFailingErrors>
<minimumTokens>100</minimumTokens>
</configuration>
</execution>
</executions>
</plugin>使用 ./mvnw help:effective-pom 和 ./mvnw dependency:resolve-plugins 核对真正加载的插件与引擎。覆盖依赖后若出现类加载错误、规则路径失效或报告格式变化,应回到已验证组合,不把 skipPmdError 之类选项当长期修复。
Gradle 明确 toolVersion 与规则文件
Gradle 自带 PMD 插件,toolVersion 选择引擎。构建工具兼容矩阵可能滞后于 PMD 独立发行版;采用 7.26.0 前要在目标 Gradle/JDK 组合验证任务、规则和报告。
plugins {
id 'java'
id 'pmd'
}
pmd {
toolVersion = '7.26.0'
ruleSets = []
ruleSetFiles = files(rootProject.file('config/pmd/ruleset.xml'))
consoleOutput = true
ignoreFailures = false
}
tasks.withType(Pmd).configureEach {
reports {
xml.required = true
html.required = true
}
}多项目仓库通过 convention plugin 应用到每个目标 source set。./gradlew pmdMain --info 用于确认引擎、规则和源码目录,根 check 则证明聚合门禁没有漏模块。生成源码、测试源码和第三方代码要在任务输入中显式分区。
显式 ruleset 降低升级漂移
引用单条规则比引用整个 category 更稳定。下面保留三个可解释规则,并排除一条暂不采用的规则:
<?xml version="1.0"?>
<ruleset name="project-java"
xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
<description>Project Java quality gate</description>
<rule ref="category/java/errorprone.xml/CloseResource"/>
<rule ref="category/java/errorprone.xml/EmptyCatchBlock"/>
<rule ref="category/java/bestpractices.xml/UnusedPrivateMethod"/>
<rule ref="category/java/design.xml">
<exclude name="LawOfDemeter"/>
</rule>
</ruleset>如果团队选择整类引用,就必须接受小版本可能加入规则的事实,并在升级差异中逐条评审。规则优先级不是业务严重度的自动映射;CI 阻断级别由团队结合误报、修复成本和风险重新定义。
正反样例要验证规则、类型与退出码
下面的资源未关闭样例应触发 CloseResource:
package lab;
import java.io.FileInputStream;
import java.io.IOException;
final class ConfigReader {
int firstByte(String path) throws IOException {
FileInputStream input = new FileInputStream(path);
return input.read();
}
}运行专项 PMD 任务,预期报告包含规则 ID、文件和位置,退出码非零。改为 try-with-resources 后使用同一版本、同一 ruleset、同一任务复跑,违规应消失。若坏样例没有命中,检查规则路径、源码语言版本、source set、优先级阈值和类型解析警告;不能通过加入更多随机规则掩盖输入问题。
再准备一个依赖外部类型的样例,用完整与故意缺失的 auxclasspath 对照。类型感知规则结果变化时,分析错误和解析警告必须进入门禁证据。PMD 解析源码并不意味着它天然拥有完整项目类型图。
CPD 的数字不能直接变成重构指标
CPD 使用 token 匹配重复片段。minimumTokens 越低越敏感,也会增加样板代码、数据类和协议生成代码的噪声。重复块需要连同文件归属、生成来源和变化耦合判断:两个恰好相似但业务变化方向不同的片段,强行抽象可能制造更差依赖。
生产代码与生成代码分别统计,语言和 token 阈值进入报告。反向样例可以复制一段超过阈值的方法,证明 cpd-check 会失败;随后抽取真正共享行为或显式排除受控生成目录,再复跑。不得用提高阈值到永不命中来“治理”存量。
incremental cache 只优化执行
PMD incremental analysis 根据输入与配置复用结果,目标是缩短时间,不是隔离历史违规。怀疑缓存损坏或配置边界错误时,用 --no-cache 或构建插件对应的全量执行做对照;两次最终违规集合应一致。缓存目录是可丢弃的加速状态,baseline 则是经过审批的债务记录,两者概念不能混用。
存量治理更适合变更文件门禁、外部差异工具或分批修复。任何 baseline/suppression 都要保存具体规则、位置或指纹、原因、owner 与到期条件,不能只允许“问题总数不增加”。
抑制与误报处置
PMD 支持 @SuppressWarnings("PMD.RuleName")、NOPMD、violationSuppressRegex 与 violationSuppressXPath。优先修代码或调整规则属性;确需抑制时选择最局部、最容易复核的机制。宽正则和 XPath 会在规则输出变化后静默扩张,必须配套正反样例。
规则误报记录包含最小复现、PMD/语言版本、规则属性、分析 classpath 和处置。升级后重跑这些样例;已修复误报应删除 suppression,不能让例外永久累积。自定义 Java Rule 会在构建进程执行,只从受控制品库加载并纳入供应链审查。
报告、升级与回滚
XML 适合机器差异,HTML 便于人工分析,SARIF 适合上传扫描平台。报告可能包含内部路径、类名和源码片段;外部 PR 的扫描与上传权限分离,制品设置访问角色与保留期。缓存、原始 SARIF 和人工摘要也应区分留存责任。
升级时旧、新 PMD 对同一源码、auxclasspath 和 ruleset 影子运行,差异按新增、消失、规则改名、位置变化和分析错误分类。PMD 主版本迁移尤其要检查废弃规则路径、XPath 版本、语言 parser 与自定义 Rule API。插件、引擎、ruleset、运行 JDK 和缓存格式作为一个回滚单元;恢复后清理输出与缓存,重新执行正反样例。
最终门禁不以“PMD 运行过”签字,而以可复核事实收口:目标 source set 全部进入分析,显式 ruleset 可追踪,类型解析警告没有被隐藏,坏样例和 CPD 反样例会阻断,修正后通过,cache 与 baseline 没有混用,升级差异和回滚版本都有记录。
