☰
PMD 标准规则编写指南:命名、消息与文档规范实战
2026/10/13 2:22:29 网站建设 项目流程
  • 静态分析
  • 代码质量

【免费下载链接】pmd

An extensible multilanguage static code analyzer.

项目地址:https://gitcode.com/gh_mirrors/pm/pmd
点击查看免费下载

导读

本文是 PMD 官方为**纳入标准发行版(standard distribution)**的规则制定的开发规范,面向想要向 PMD 提交新规则或重构既有规则的贡献者。读完本文,你将掌握 PMD 规则在命名、违规消息、规则描述与示例注释上的具体要求,理解"以底层问题而非实现方式命名"的核心理念,并能依据规则描述模板写出评审友好、可长期维护的规则文档。文中的正反示例均可在当前仓库的规则定义文件中找到实际对应,方便对照学习。

[!NOTE] 需要说明的是,这套指南是 2021 年(6.38.0)新引入的,目前大部分存量规则尚未完全遵循,目标是最终所有规则都按此标准更新。因此本文既是新规则的编写规范,也是既有规则重构的参考清单。

为什么需要这套指南?

在原文档(rule_guidelines.md)中,PMD 团队明确了两点动机:

  • 防止低质量贡献(prevent low quality contributions):通过事先约定标准,避免提交者凭个人风格写出难以评审、难以维护的规则;
  • 减少规则评审时间(reduce time reviewing rules):有了统一的命名与文档要求,维护者可以更快地评估一条规则是否值得合并。

同时,指南有明确的作用范围:只适用于纳入标准发行版(stock PMD)的规则。社区自用的自定义规则不受约束,但如果希望规则被官方接受、随 PMD 一起分发,就必须满足下面的准入要求。

标准规则的准入要求

要成为 PMD 标准发行版中的一条规则,需要同时满足四点:

  1. 广泛适用性(Broad applicability):规则可以针对某个特定框架,但该框架必须是广泛使用的——冷门框架专属的规则不会被采纳;
  2. 扎实的文档(Solid documentation):文档质量有明确底线,具体模板见下文"规则描述模板"一节;
  3. 性能规则必须附带扎实的基准测试(solid benchmarks):如果你提交的是性能类规则,必须用可复现的基准数据证明其收益,且不接受微优化规则(no micro-optimization rules);
  4. 与其他规则无重叠(No overlap with other rules):新规则不得和现有规则检测同一类问题,否则会造成重复告警和规则集混乱。

这四条在仓库中也有印证:PMD 的规则按语言分门别类地定义在各语言的category/*.xml中(如 bestpractices.xml、errorprone.xml),每条规则都有since(引入版本)、priority(优先级)、message和example字段——"扎实文档"正是由这些字段共同支撑的。

规则命名的 Do/Don't

命名是原文档着墨最多、也最能体现 PMD 规则设计哲学的部分。核心原则一句话:规则名应命名"违规所暴露出的底层问题",而不是"当前检测手段"。

Don't:不要把实现放进名字

把实现细节写进规则名,一旦规则检测范围演进,名字就会"说谎",而且改名会造成巨大的兼容性成本。原文档给了两个经典反例:

  • SwitchStmtShouldHaveDefault:在枚举(enum)成为 Java 一等公民后,switch 枚举分支不再"必须"有 default,只要分支覆盖所有枚举常量就算穷尽(exhaustive)。此时这条规则的名字已经与事实不符——它检测的其实是"switch 不穷尽"这一更一般的问题;
  • MissingBreakInSwitch:名字暗示它是通过"数 break 数量"来判断 fall-through 的,但实际上它检测的是"switch 分支隐式贯穿(fall-through)"这一行为。因为名字把检测手段写死了,团队无法在不改名的情况下改变实现。

这两个反例在仓库中都有真实落点:SwitchStmtsShouldHaveDefault已被标记为deprecated并重定向到NonExhaustiveSwitch(见 bestpractices.xml 中的<rule name="SwitchStmtsShouldHaveDefault" deprecated="true" ref="NonExhaustiveSwitch"/>);MissingBreakInSwitch则是在 PMD 6.37.0 改名为ImplicitSwitchFallThrough,规则描述中明确写了这一改名记录(见 errorprone.xml)。

Do:命名底层问题

好的命名应当直接点出"问题本身",再用描述和示例解释它为什么是问题、如何修复:

  • 用NonExhaustiveSwitchStatement替代SwitchStmtShouldHaveDefault:名字直接表达"switch 不穷尽"这一事实,修复方式(补 default、补分支,或未来其他手段)留给描述展开;
  • 用SwitchCaseFallsThrough替代MissingBreakInSwitch:直接描述"case 分支贯穿"这一违规行为。

实际落地时,PMD 选择了NonExhaustiveSwitch作为最终规则名,其 XPath 实现也恰好体现了"不依赖单一路径"的设计(详见下文"源码佐证"):

<rule name="NonExhaustiveSwitch" since="1.0" message="Switch statements or expressions should be exhaustive, add a default case (or missing enum branches)" class="net.sourceforge.pmd.lang.rule.xpath.XPathRule"> <description> Switch statements should be exhaustive, to make their control flow easier to follow. This can be achieved by adding a `default` case, or, if the switch is on an enum type, by ensuring there is one switch branch for each enum constant. This rule doesn't consider Switch Statements, that use Pattern Matching, since for these the compiler already ensures that all cases are covered. The same is true for Switch Expressions, which are also not considered by this rule. </description> <priority>3</priority> <properties> <property name="xpath"> <value><![CDATA[ //SwitchStatement (: exclude empty switches :) [count(*) > 1] [@DefaultCase = false()] [@ExhaustiveEnumSwitch = false()] (: exclude pattern tests - for these, the compiler will ensure exhaustiveness :) [not(*/SwitchLabel[@PatternLabel = true()])] ]]></value> </property> </properties> ... </rule>

注意其中用 XPath 注释((: ... :))逐行解释了每个条件的意图——这正是原文档"要注释你的 XPath 表达式"要求的活范例。

Don't:为同一问题创建多条规则

原文档指出,EmptyIfStmt和EmptyWhileStmt本质是同一个问题:语法树中存在无用(空)的语法结构。把它们拆成两条规则只会让规则集膨胀、用户困惑。

这一点 PMD 已经给出了终极解法:在 6.46.0 引入了EmptyControlStatement,用一条规则统一检测空try/finally/switch/synchronized/if/各类循环/初始化器/用作语句的块,并明确声明它取代了 EmptyFinallyBlock、EmptyIfStmt、EmptyInitializer、EmptyStatementBlock、EmptySwitchStatements、EmptySynchronizedBlock、EmptyTryBlock、EmptyWhileStmt 共八条旧规则(见 codestyle.xml)。规则定义中的注释甚至记录了命名过程的取舍——为什么不用EmptyCodeBlock、EmptyStatement、EmptyCodeConstruct——这正是"命名要反复推敲"的又一佐证。

Don't:把名字局限在当前能力范围内

UnusedPrivateField是原文档给出的反例:真正的问题是"存在未使用的字段",private只是当前检测能力的边界。如果哪天 PMD 具备检测包私有(package-private)未使用字段的能力,名字叫UnusedField就能平滑扩展,而UnusedPrivateField则不得不改名。

仓库中这条规则的实际描述也印证了其能力边界在演进:自 PMD 6.50.0 起,带注解的字段或带注解的类中的私有字段不再报告,因为注解常被 DI、Mock、Lombok 等框架通过反射使用,静态分析无法追踪(见 bestpractices.xml 中UnusedPrivateField的描述)。能力边界在变,名字更要留出余地。

规则消息的 Do/Don't

违规消息(message)是用户在报告中第一眼看到的内容,直接影响体验。三条原则:

  1. Do:中性指出问题或需要复查的结构。例如"Unnecessary parentheses"(不必要的括号)——陈述事实,不带情绪、不下指令;
  2. Don't:使用命令式口吻。例如"Avoid unnecessary parentheses"——除非给出理由,否则"避免 xxx"只会让人反感;仓库中大量存量规则的 message 仍以 "Avoid" 开头(如UnusedPrivateField的"Avoid unused private fields such as ''{0}''."、UnusedLocalVariable的"Avoid unused local variables such as ''{0}''."),这正是"存量规则尚未完全遵循新指南"的直观体现,也说明新规则应避免再犯;
  3. Don't:写同义反复(tautological)的消息。例如"Unnecessary parentheses should be removed"——读者看到只会反问"是啊,所以呢?"。消息应当补充信息量,而不是复述问题本身。

可以对照NonExhaustiveSwitch的消息写法——它不只是说"switch 不穷尽",还给出了修复方向:"add a default case (or missing enum branches)",信息密度明显更高。

描述、示例与注释要求

  • Do:规则描述使用 Markdown 格式,并在合理位置按约 80 个字符换行,保证在命令行、网页和源码三种场景下都有良好可读性。上文的NonExhaustiveSwitch描述就是范例;
  • Do:为规则示例写详尽的注释,让读者一眼看清"该看哪里"、哪里是违规、哪里是修复后。仓库里ImplicitSwitchFallThrough的示例堪称范本:
public void bar(int status) { switch(status) { case CANCELLED: doCancelled(); // break; hm, should this be commented out? case NEW: doNew(); // is this really a fall-through? // what happens if you add another case after this one? case REMOVED: doRemoved(); // fallthrough - this comment just clarifies that you want a fallthrough case OTHER: // empty case - this is interpreted as an intentional fall-through case ERROR: doErrorHandling(); break; } }

这个示例(见 errorprone.xml 中ImplicitSwitchFallThrough)不仅演示了违规与合法写法,还借助注释把"为什么这是问题"(新增 case 后行为不可预期)、"如何合法贯穿"(// fallthrough注释)和"哪些 case 自动豁免"(空 case 视为有意贯穿)都交代清楚了——注释本身就是规则文档的一部分;

  • Do:注释 XPath 表达式。XPath 规则的可读性全靠注释,NonExhaustiveSwitch中每行谓词前的(: ... :)注释就是规范要求下的产物。

规则描述模板(Rule description template)

原文档给出了标准规则描述必须遵循的六段式结构,这是"扎实文档"的具体定义:

  1. 一句话概要:规则报告什么(What the rule reports),一行说清;
  2. 存在理由与适用场景:为什么要有这条规则、在哪些场景有用,包括自哪个语言版本起适用等上下文;
  3. 空行:分隔"理由"与"假设";
  4. 假设与关键词定义:解释上文中使用的所有假设和关键词。例如"被覆写的方法会被忽略"、"就本规则而言,'可见'字段指非私有字段"——读者必须能在不读实现的情况下精确理解规则的判定边界;
  5. 已知局限:如实描述规则的检测盲区(known limitations),避免用户误信"全量覆盖";
  6. 空行;
  7. 每个属性的用途说明:逐个解释规则属性(property)如何修改上述假设、为什么要用。原文档特别强调:"如果你解释不了它为什么存在,那它就不该存在!"

注意第 4 点对"关键词定义"的强调与NonExhaustiveSwitch的实际描述完全一致——规则描述中专门界定了"本规则不考虑使用模式匹配的 switch 语句与 switch 表达式,因为编译器已保证其穷尽性",这就是"明确假设与边界"的规范写法。规则的属性定义同样如此,例如LongVariable规则的minimum属性带有min="1" max="100"与默认值17及完整用途描述(见 codestyle.xml),保证使用者无需读源码即可配置。

源码级佐证:穷尽性判断到底怎么实现

NonExhaustiveSwitch的 XPath 用到了两个 AST 属性@DefaultCase与@ExhaustiveEnumSwitch,它们的真实语义定义在 ASTSwitchLike.java 中:

  • isExhaustiveEnumSwitch():当 switch 的测试表达式类型可解析为枚举时,比较"枚举常量总数"与"所有分支标签的子节点总数",两者相等即为穷尽;类型无法解析时返回false(对应 XPath 谓词[@ExhaustiveEnumSwitch = false()]会命中这类"无法证明穷尽"的 switch);
  • isExhaustive():更一般化的判断,处理了模式匹配(有 type pattern 且无 default 时编译器已保证穷尽)、sealed 类型与布尔类型(Java 23 预览 JEP 455)等更现代的穷尽性场景——这正是"规则能力会随语言演进,命名要留余地"的底层体现。

规则并非只有定义文件:每个标准规则都有配套的规则测试。NonExhaustiveSwitch的测试类 NonExhaustiveSwitchTest.java 继承PmdRuleTst,实际的测试用例写在 XML 数据文件中(NonExhaustiveSwitch.xml),每个<test-code>声明expected-problems(期望命中数)与代码片段,例如:

  • 无 default 的 switch →expected-problems=1(违规);
  • 空 switch →expected-problems=0(不违规,注释说明空 switch 由EmptyControlStatement负责);
  • 带 default 的 switch →expected-problems=0(合法);
  • 枚举 switch 未覆盖全部常量 →expected-problems=1;覆盖全部常量 →expected-problems=0。

这套"XML 定义规则 + XML 驱动测试"的机制,意味着遵循本文指南的规则天然可被 CI 验证——新规则的每个行为声明(描述、示例、假设)背后都有可执行用例背书。想深入了解规则开发的完整流程,可以继续阅读 writing_pmd_rules.md 及其分册(Writing XPath Rules、Defining Properties、Testing your Rules)。

小结:提交前的自检清单

综合原文档与仓库实践,提交一条标准规则前请逐项确认:

检查项要求仓库对照
广泛适用性通用问题或广泛使用的框架专属问题—
性能规则有扎实基准,拒绝微优化—
规则重叠与现有规则检测问题不重复EmptyControlStatement 合并八条空语句规则
规则命名命名底层问题,不含实现、不限当前能力NonExhaustiveSwitch 替代SwitchStmtsShouldHaveDefault
违规消息中性、非命令式、非同义反复对照UnusedPrivateField的旧式 "Avoid..." 写法
描述与示例Markdown、80 字符换行、示例详注、XPath 注释ImplicitSwitchFallThrough示例
描述模板概要 → 理由 → 假设与关键词 → 局限 → 属性说明NonExhaustiveSwitch描述
测试提供覆盖各分支的 XML 测试用例NonExhaustiveSwitch.xml

这套指南的本质,是让"规则"从一段能跑的逻辑,升格为一份可评审、可继承、可长期演进的工程资产——命名负责面向未来,消息负责面向用户,文档负责面向维护者,测试负责面向回归。新规则从第一天起就按这个标准写,存量规则则按此清单逐步重构,最终使整个标准规则库保持一致的品质基线。

  • 静态分析
  • 代码质量

【免费下载链接】pmd

An extensible multilanguage static code analyzer.

项目地址:https://gitcode.com/gh_mirrors/pm/pmd
点击查看免费下载

相关推荐

上一篇:gte-large-zh-openmind vs 主流中文嵌入模型:66.72分CMTEB成绩背后的真相
下一篇:如何用SmolLM实现经济高效的AI推理?

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询