[TOC]
概述
最近想自定義 PMD 規(guī)則來檢測(cè)java代碼,發(fā)現(xiàn) XPath 挺好用的,剛好也解析出來了一個(gè) lambda 表達(dá)式的校驗(yàn),所以分享一下
0x1 概念介紹
AST
根據(jù) PMD規(guī)則介紹得知

大概意思就是:
在運(yùn)行規(guī)則之前,PMD 會(huì)把源文件格式化為 AST (抽象語法樹) 。 這個(gè)技術(shù)很多地方用到了,如 Eclipse 、 IDEA、JavaParser) 。這棵樹可以體現(xiàn)出來代碼的語法結(jié)構(gòu), 并且比源碼的信息還要豐富和規(guī)范,所以 PMD 是直接拿著規(guī)則去匹配 AST
XPath
AST 在 PMD 中被解析出來之后,格式是 xml 的,想去搜索里面的信息,就要用到 XPath 的技術(shù)
其實(shí)就是一種搜索 xml 元素的技術(shù),類似的技術(shù)還有 JQuery 搜索 html
語法介紹在這里 XPath語法
大概看一下 // / [] 就行,其它的用到再看
0x2 開發(fā)
pmd 支持的有兩種
- XPath
- Java Visit
用 XPath 的配置規(guī)則的話,需要看的文檔少,上手快,我選的就是這個(gè)
不過文檔從哪里看起呢?先不著急,看看別人是怎么玩的
p3c-pmd
阿里內(nèi)置了很多規(guī)則,有的就是用 XPath 實(shí)現(xiàn)的,我們挑一個(gè)看看
AvoidPatternCompileInMethodRule
代碼截圖

代碼的作用是: 不能在方法體內(nèi)部出現(xiàn) Pattern.compile("xx"),因?yàn)檎齽t表達(dá)式編譯很消耗性能,應(yīng)該在常量里做這個(gè)步驟
我們可以看到規(guī)則的實(shí)現(xiàn)代碼很簡(jiǎn)單,其中紅框部分就是檢測(cè)代碼的規(guī)則
我摳出來格式化一下
//MethodDeclaration//PrimaryExpression
[PrimaryPrefix/Name[@Image='Pattern.compile']
and
PrimarySuffix/Arguments/ArgumentList/Expression
/PrimaryExpression/PrimaryPrefix/Literal[@StringLiteral='true']]
Pattern.compile("xx")
逐一分析一下
//MethodDeclaration從根節(jié)點(diǎn)開始,選擇所有 MethodDeclaration
//PrimaryExpression在所有的 MethodDeclaration 的下面,繼續(xù)跳著選 PrimaryExpression
[到末尾]篩選 PrimaryExpression 節(jié)點(diǎn)必須滿足后續(xù)兩個(gè)條件
PrimaryPrefix/Name[@Image='Pattern.compile']
PrimaryExpression 有一個(gè)子元素 PrimaryPrefix, 且 PrimaryPrefix 的子元素 name 值為 Pattern.compilePrimarySuffix/Arguments/ArgumentList/Expression/PrimaryExpression/PrimaryPrefix/Literal[@StringLiteral='true']
PrimaryExpression 有一個(gè)子元素 PrimarySuffix,這個(gè)子元素有個(gè)子元素Arguments,這個(gè)子元素有個(gè)子元素 ArgumentList,一直套娃到 PrimaryPrefix, PrimaryPrefix的子元素Literal滿足條件 @StringLiteral='true'
簡(jiǎn)單翻譯過來就是,方法是 Pattern.compile, 這個(gè)方法有入?yún)⑶沂亲址?/p>
看一下語法樹

- 橙色是快速選擇的
// - 紅色是條件1
- 藍(lán)色是條件2
lambda
我們接下來研究一下 lambda 的 AST (注意:我是帶著目的來研究特定場(chǎng)景、特定寫法的 lambda 的,結(jié)論不一定適合所有場(chǎng)景)
我的場(chǎng)景是 避免工具類濫用
B layer1 = Null.of(() -> a.getB());
C layer2 = Null.of(() -> a.getB().getC());
Integer layer3 = Null.of(() -> a.getB().getC().getNumber());
我們的工具類方法 Null.of 里面會(huì)對(duì) NullPointerException 進(jìn)行捕獲,在捕獲到的時(shí)候返回一個(gè)默認(rèn)值
這個(gè)工具類主要是為了在處理獲取嵌套字段的時(shí)候書寫方便, 比如 Null.of(() -> a.getB().getC().getNumber()), 如果自己手寫 if (null != xxx) 需要寫很多層判斷,代碼會(huì)很惡心。
工具類的本意是好的,不過后面大家濫用了,在一層的時(shí)候也使用工具類。因?yàn)閘ambda表達(dá)式內(nèi)部有可能會(huì)發(fā)生空指針異常,而發(fā)生異常時(shí),因?yàn)閖vm要停下來獲取線程棧快照并填充到異常棧里,所以工具類性能較低,所以希望大家在一層的時(shí)候,自己手寫。
好,背景介紹完了,現(xiàn)在要分析一下語法了,先看一下對(duì)比圖

第二行相比第一行,代碼多了.getc(), AST 多了兩個(gè) PrimarySuffix 元素
第三行相比第二行,代碼多了.getNumber(), AST 多了兩個(gè) PrimarySuffix 元素
所以 lambda 的 AST 大概規(guī)律如下:

所以我們要找的代碼Null.of(() -> a.getB())規(guī)律就是: suffix元素有且僅有一個(gè)
(圖是用 latex畫的)latex公式
開發(fā)自定義規(guī)則
規(guī)則
根據(jù) 阿里的 p3c-pmd 照葫蘆畫瓢,可以得到 XPath 表達(dá)式
//MethodDeclaration//PrimaryExpression[
PrimaryPrefix/Name[@Image='Null.of']
and
PrimarySuffix/Arguments/ArgumentList/Expression/PrimaryExpression/PrimaryPrefix/LambdaExpression
/Expression/PrimaryExpression[count(PrimarySuffix)=1]
]
核心就是 PrimaryExpression[count(PrimarySuffix)=1]
開發(fā)
Java 代碼的話,理解都不用理解,直接把 阿里的 AvoidPatternCompileInMethodRule 文件內(nèi)容復(fù)制一下,然后改個(gè)名字,改一下 XPath, 改一下提示信息就可以了

還需要配置 規(guī)則xml, 簡(jiǎn)單的放在 阿里的文件里面就行,很簡(jiǎn)單就不贅述了
打包之后,運(yùn)行

大功告成