当今的软件开发需要使用许多不同的工具和技术来确保代码质量和稳定性。PMD是一个流行的静态代码分析工具,可以帮助开发者在编译代码之前发现潜在的问题。在本文中,我们将讨论如何在Gradle中使用PMD,并介绍一些最佳实践。
什么是PMD?
PMD是一个用于Java代码的静态代码分析工具。它可以帮助开发者找出潜在的问题,如代码重复、未使用的变量、错误的异常处理等。PMD支持多种规则,可以根据具体项目的需要进行配置。其工作原理参考How PMD Works。
PMD支持通过命令行界面(CLI, Command Line Interface for batch scripting)和其他多种集成方式,比如Maven、Gradle、Java API等等。
PMD在Gradle中配置和使用
Gradle中自带了PMD插件,插件的默认版本可以通过源码DEFAULT_PMD_VERSION知道。使用和配置可以参考The PMD Plugin,页面左上角可以选择Gradle版本,确保查看的版本和你使用的Gradle版本一致,因为很多PMD的配置属性或者功能不一定在每个版本都有。
通过页面左上角选了其他版本后跳转的地址是Gradle文档的首页,而不是PMD插件的文档页。我们可以通过修改The PMD Plugin链接中的8.0.2
为其他版本号即可跳转到对应Gradle版本包含的PMD插件文档的页面。比如:
当前最新版:https://docs.gradle.org/current/userguide/pmd_plugin.html
7.3.3版本:https://docs.gradle.org/7.3.3/userguide/pmd_plugin.html
在项目build.gradle文件中增加以下内容应用插件和扩展PMD,参考Usage和Configuration,更多的配置属性可以参考PmdExtension。
plugins {
id 'pmd'
}
pmd {
// 是否将 PMD 结果输出到终端
consoleOutput = true
// 要使用的PMD版本
toolVersion = "6.21.0"
// 规则优先级阈值,低于这个优先级则会被忽略
rulesMinimumPriority = 5
// 使用的规则集配置文件路径
ruleSets = ["category/java/errorprone.xml", "category/java/bestpractices.xml"]
}
插件会生成两个主要的PMD TaskpmdMain和pmdTest分别对main和test两个项目源文件目录使用PMD进行代码检查。
找到IDEA Gradle窗口 > Tasks > other
,双击生成的Task;或者在项目根目录运行./gradlew pmdMain
都可以运行PMD。检查结果将输出到终端中(前提是配置了consoleOutput = true
),违反了PMD规则的类会给出完整的跳转路径以及规则提示信息。
最后还会给出一个报告的地址,内容包含了输出到终端的信息。Problem列出了规则的提示,点击可以跳转到PMD规则描述文档对应的位置。
Gradle PMD Plugin扩展属性
在这里我们将PMD插件的扩展属性作用进行说明,参考PmdExtension,这个文档详细说明了各个属性的作用、默认值和配置示例。如果文档描述的不是很清楚也可以参考PMD CLI options的对应描述。
consoleOutput
是否将结果输出到终端(System.out
)允许值为true|false
。
ignoreFailures
如果出现了警告,是否允许继续构建,允许值为true|false
。
配置为否(false),在执行build的时候(build任务中默认包含了pmdMain和pmdTest),如果发现了代码有违反规则,将会中断构建过程;配置为是(true),将不会中断构建,只是输出报告信息。
maxFailures
停止构建前允许的最大失败次数。
incrementalAnalysis
是否开启增量分析,允许值为true|false
。在pmd docs Incremental Analysis中详细描述了增量分析的相关信息。简单来说,开启了增量分析,PMD会缓存分析数据和结果,后续分析仅查看那些新的/已更改的文件,以此显著减少分析的时间,在Gradle中,这个功能使用PMD6.0.0
及以上版本才有。
但是有一些情况会导致增量分析的缓存失效:使用PMD的版本发生了变化;使用的规则集已更改;被分析的代码的类路径已更改;被分析代码依赖的库的类路径已更改。具体参考When is the cache invalidated?
在以上前提下,即使切换分支缓存也是有效的,甚至还支持在不同的机器重复使用缓存文件。参考Can I reuse a cache created on branch A for analyzing my project on branch B? 和Can I reuse a cache file across different machines?
reportsDir
报告生成的路径。
ruleSetFiles
要使用的自定义规则集文件路径,可以在files()
中填多个路径。
ruleSetFiles = files("config/pmd/myRuleSet.xml")
ruleSetConfig
跟ruleSetFiles
的作用一样,不过只能填一个文件路径。
ruleSetConfig = resources.text.fromFile("config/pmd/myRuleSet.xml")
ruleSets
指定使用的规则集,默认值为["category/java/errorprone.xml"]。
建议如果配置了ruleSetFiles
或者ruleSetConfig
,就将ruleSets
配置为空(ruleSets = []
),以免互相干扰,官方文档Custom ruleset给出的例子也是如此。
ruleSets = ["category/java/errorprone.xml", "category/java/bestpractices.xml"]
rulesMinimumPriority
每个规则都有个优先级,是从 1 到 5 的整数,其中 1 是最高优先级,参考[Message and priority overriding,每个规则的优先级参考Java Rules。rulesMinimumPriority
的作用是配置报告的最低优先级,低于这个优先级的规则将被忽略。比如配置rulesMinimumPriority = 4
,优先级为 5 的规则将被忽略。
sourceSets
作为 check
和 build
任务的一部分进行分析的源代码集合,配置方式参考SourceSet。
targetJdk
PMD使用的JDK版本。有些规则可能会要求JDK的最低或者最高版本,具体要求参考Java Rules。
threads
PMD 运行时使用的线程数。
toolVersion
要使用的PMD的版本。
为项目自定义合适的规则集
规则分类和查找
PMD能检测的语音有很多种(后面内容以Java为例),针对不同的语音,PMD内置了很多检测规则,并归为了以下几个类别:
- 最佳实践(Best Practices):这些规则执行普遍接受的最佳实践。
- 代码风格(Code Style):这些规则强制执行特定的编码风格。
- 设计(Design):帮助您发现设计问题的规则。
- 文档(Documentation):这些规则与代码文档有关。
- 容易出错(Error Prone):用于检测损坏、极度混乱或容易出现运行时错误的构造的规则。
- 多线程(Multithreading):这些是在处理多个执行线程时标记问题的规则。
- 性能(Performance):标记次优代码的规则。
- 安全性(Security):标记潜在安全漏洞的规则。
在Java Rules列出了所有相关的规则,点击蓝色字符可以跳转到规则的详细描述页面。
下图是规则AbstractClassWithoutAbstractMethod文档描述的信息,其他规则的描述可能还会包含JDK版本的要求,其他可配置属性等等。
需要注意有的规则可能被标记为Deprecated
代表被弃用了。
配置规则集
我们可以编辑XML格式的规则集文件,指定我们项目要执行的规则,参考Making rulesets。下面是没有包含任何规则的规则集文件的模版。
<?xml version="1.0"?>
<ruleset name="Custom Rules"
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>
My custom rules
</description>
<!-- Your rules will come here -->
</ruleset>
从上文我们可以知道PMD内置的每个规则都会提供引用实例,我们引用单个规则的时候,只需要将示例的XML代码复制到规则集文件中即可。
<rule ref="category/java/errorprone.xml/EmptyCatchBlock" />
从ref
中填写的路径category/java/bestpractices.xml/AbstractClassWithoutAbstractMethod
我们可以明显看到它是按照内置规则集文件路径/规则名称的格式
组织的,一个内置规则集文件对应了一个分类。
我们可以引用内置规则集文件实现批量引入分类下的所有规则,每个分类对应的XML文件名可以参考GitHub pmd-java resources。再通过exclude
指定规则的名称来排除某些规则。
<rule ref="category/java/codestyle.xml">
<exclude name="WhileLoopsMustUseBraces"/>
<exclude name="IfElseStmtsMustUseBraces"/>
</rule>
我们可以使用exclude-pattern
排除某些文件,使其不被PMD检查,也可以使用include-pattern
包含的方式。如果两种方式都包含相同的文件,最终这个文件会被PMD检查。
<?xml version="1.0"?>
<ruleset name="myruleset"
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>My ruleset</description>
<exclude-pattern>.*/some/package/.*</exclude-pattern>
<exclude-pattern>.*/some/other/package/FunkyClassNamePrefix.*</exclude-pattern>
<include-pattern>.*/some/package/ButNotThisClass.*</include-pattern>
<!-- Rules here ... -->
</ruleset>
规则集文件编辑好后,使用ruleSetFiles
或者ruleSetConfig
配置路径。比如下面配置的意思是指向了项目根目录下的/code-analysis/pmd/rulesets/custom-rule.xml
。
ruleSetFiles = files("${project.rootDir}/code-analysis/pmd/rulesets/custom-rule.xml"
配置规则
规则引用的同时,我们可以覆盖其原有的一些配置,比如提示消息message
和优先级priority
。
<rule ref="category/java/errorprone.xml/EmptyCatchBlock"
message="Empty catch blocks should be avoided" >
<priority>5</priority>
</rule>
某些规则可能有特定的属性,我们也可以将其覆盖。这些特定的属性Java Rules中都有提供,比如下面这个例子参考NPathComplexity。
<rule ref="category/java/design.xml/NPathComplexity">
<properties>
<property name="reportLevel" value="150"/>
</properties>
</rule>
有些属性可以提供多个值,这种情况下可以通过分隔符来提供,比如竖线(|
)或逗号(,
)。
<property name="legalCollectionTypes"
value="java.util.ArrayList|java.util.Vector|java.util.HashMap"/>
抑制警告
有时候PMD可能会产生误报,这种时候我们可以通过抑制警告让PMD跳过对这些代码的检查。
从Java 1.5开始可以使用注解@SuppressWarnings
来标记类或者方法。
@SuppressWarnings('PMD')
抑制所有PMD的警告。@SuppressWarnings("PMD.UnusedLocalVariable")
抑制规则UnusedLocalVariable
的警告。@SuppressWarnings({"PMD.UnusedLocalVariable", "PMD.UnusedPrivateMethod"})
抑规则UnusedLocalVariable
和UnusedPrivateMethod
的警告。@SuppressWarnings("unused")
JDK里面的unused
PMD也遵守,抑制所有跟未使用相关的警告。比如:UnusedLocalVariable
和UnusedPrivateMethod
。
在警告提示的代码行的末尾加上注释// NOPMD
也可以抑制这一行引起的警告,参考NOPMD。
在规则集文件中也可以配置要抑制警告的文件,匹配的方式可以是正则表达式或者XPath,具体可以了解The property violationSuppressRegex和The property violationSuppressXPath。
第三方规则集
除了PMD内置的规则集,我们还可以引入第三方规则集。在3rd party rulesets中列出了一些,还有阿里Java开发规范p3c也基于PMD开发一套规则集,从它的pom.xml可以了解到是基于PMD6.15.0
版本。
参考Dependency management引入规则集依赖,在规则集配置中引入提供的规则即可。
dependencies {
pmd "com.alibaba.p3c:p3c-pmd:2.1.1"
}
需要注意的是,第三方的规则集很可能没有按照PMD内置规则集那样分类,它们提供的规则配置文件目录也可能不一样,比如p3c的规则配置文件都在/resources/rulesets/目录下并独自定义了一套分类。
其他技巧
PMD的最新官方文档地址是:https://docs.pmd-code.org/latest/pmd_userdocs_tools.html。链接中的latest
对应了版本号,指向的是当前最新版本,如果想查看其他版本的文档,修改为对应的版号即可。比如6.39.0
版本的链接为:https://docs.pmd-code.org/pmd-doc-6.39.0/index.html。不过可能只有比较新的一些版本才能看到对应的文档。
官方提供了一个PMD的最佳实践可以了解下。
PMD还有跟特定语言相关的文档,比如Java support,里面有支持的JDK版本等信息。
如果使用过程中遇到了问题,可以参考Getting Help从这些网站里面寻找帮助github discussions、github issues、stackoverflow tagged pmd。
PMD官方文档还提供了Copy/Paste Detector (CPD)关信息,CPD可以用于检测重复代码。还提及了Duplicate Code教我们遇到重复代码如何消除,以及一个关于设计模式的网站Design Patterns。
关于PMD这个名字,并没有特殊的含义,作者纯粹只是觉得这几个字母放一起作为名称挺好的,来自What does ‘PMD’ mean?
参考
GitHub - pmd
PMD - docs
Gradle - The PMD Plugin