p3c-pmd扩展开发自定义规则

阿里的pmd项目遵循pmd的规范开发,引用了ast的依赖包做ast树的处理,要在此开源项目基础上做扩展开发首先要明白,静态代码检查受限于ast语法树以及代码结构的原因,很难检测关联性比较强的代码,例如:
类a使用spring注入的方式获取实例,但该bean是在另一个配置类中,此时pmd如果要检测该实例的相关配置,是很难检测的,pmd主要检测静态代码层面,也就是当前类能看到的东西,如果想要去检测类a的实例的某个属性是否符合规范,是很难的。
主要是受限于pmd检测时无法指定检测顺序,且检测时每个类都会进行build maven plugin中所配置的检查项,如果要规范产品线上分布式事务与本地事务的注解问题等,个人认为应该放到测试阶段去检测,可以使用市面常见的混沌测试工具,例如ChaosBlade,对各种可能造成故障的问题代码进行一个故障模拟。

p3c-pmd开发流程如下

1、了解:

官方的pmd项目中默认提供了ast语法树的分析工具

下载地址如下:github.com/pmd/pmd

注意:此工具运行需要JDK10以上及JAVAFX

JAVAFX下载地址如下:download2.gluonhq.com/openjfx/19/...

解压此包之后,添加环境变量:

编辑

编辑

解压pmd-bin包,目录如下:

编辑

点击designer.bat可以启动ast语法分析工具

编辑

中间为源代码,例如我们认为正确的代码,右侧为分析出的ast语法树,从右侧语法树中可以看出相应的节点结构,可以看到有相关的节点例如:

  • CompilationUnit根节点
  • TypeDeclaration 类信息
  • Annotation 接口信息
  • MemberValuePairs 接口参数信息
  • FieldDeclaration 字段属性信息
  • MethodDeclarator 方法相关信息

等等,在编写代码规范规则时,要梳理出正确的代码,并在工具中分析ast语法树,对ast树层层遍历处理校验,即可

2、项目

p3c-pmd结构大体介绍如下:

编辑

以我这边的一个例子来说:

ini 复制代码
package com.alibaba.p3c.pmd.lang.java.rule.exception;

import com.alibaba.p3c.pmd.lang.java.rule.AbstractAliRule;
import io.seata.spring.annotation.GlobalLock;
import net.sourceforge.pmd.lang.java.ast.*;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.stream.Collectors;

/**
 * @classDesc: 功能描述:(检测事务注解是否也加了GlobalLock或GlobalTransaction)
 * @author: cy
 * @date: 2022/10/11 10:03
 */
public class TransactionMustHaveGlobalLockOrGlobalTransactional extends AbstractAliRule {
    private static final Integer ANNOTATION_COUNT = 3;

    @Override
    public Object visit(ASTCompilationUnit node, Object data) {
        //获取包括类注解在内的当前类所有注解
        List<ASTAnnotation> allAnnotations = node.findDescendantsOfType(ASTAnnotation.class);
        //以下是方法体的节点列表
        List<ASTClassOrInterfaceBodyDeclaration> methods = node.findDescendantsOfType(ASTClassOrInterfaceBodyDeclaration.class);
        //只保留类注解
        List<ASTAnnotation> allClassAnnotations = allAnnotations.stream()
                .filter(e -> e.getFirstParentOfType(ASTClassOrInterfaceBodyDeclaration.class) == null)
                .collect(Collectors.toList());
        //所有类注解类型
        List<? extends Class<?>> allClassAnnotationType = allClassAnnotations.stream().map(ASTAnnotation::getType).collect(Collectors.toList());
        boolean flag = false;
        int count = 0;

        for (Class<?> aClass : allClassAnnotationType) {
            boolean flagA = aClass == AnnotationConstant.GLOBAL_LOCK.getClassZz();
            boolean flagB = aClass == AnnotationConstant.GLOBAL_TRANSACTIONAL.getClassZz();
            boolean flagC = aClass == AnnotationConstant.TRANSACTIONAL.getClassZz();
            //类是否包含全局注解
            if (flagA || flagB || flagC) {
                count++;
                flag = true;
            }
        }
        if (count == ANNOTATION_COUNT) {
            return super.visit(node, data);
        }
        //遍历所有方法节点
        for (ASTClassOrInterfaceBodyDeclaration method : methods) {
            //获取方法所有注解
            List<ASTAnnotation> methodAnnotation = method.findChildrenOfType(ASTAnnotation.class);
            //如果方法上有本地事务注解检查方法上或类上是否有加全局事务或锁注解
            List<ASTAnnotation> collect = methodAnnotation.stream().filter(e -> e.getType().equals(AnnotationConstant.TRANSACTIONAL.getClassZz())).collect(Collectors.toList());
            if (!collect.isEmpty()) {
                //方法上全局事务或全局锁注解列表
                List<ASTAnnotation> lockOrTransactionAnnotations = methodAnnotation.stream().filter(e -> e.getType().equals(AnnotationConstant.GLOBAL_TRANSACTIONAL.getClassZz()) ||
                        e.getType().equals(AnnotationConstant.GLOBAL_LOCK.getClassZz())).collect(Collectors.toList());
                //方法注解不包含@GlobalLock or @GlobalTransaction 且类注解也不包含
                if (lockOrTransactionAnnotations.isEmpty() && !flag) {
                    ASTMethodDeclaration methodNode = method.getFirstDescendantOfType(ASTMethodDeclaration.class);
                    addViolationWithMessage(data, methodNode,
                            "java.exception.MethodTransactionMustHaveGlobalLockOrGlobalTransactionRule.msg",
                            new Object[]{methodNode.getMethodName()});
                }
            }
        }
        return super.visit(node, data);
    }

}

所有规则类继承了AbstractAliRule,AbstractAliRule继承了AbstractJavaRule,在往上继承了JavaParserVisitor接口,AbstractJavaRule实现了JavaParserVisitor接口所定义的一系列访问ast树的方法,全部提供了一种默认的访问检查方式,同时提供给子类继承重写的能力,AbstractAliRule再次做了一次封装,此方式看起来是访问者模式(没仔细看,应该是)

例如我们要实现对类的根节点开始处理,我们就实现:

typescript 复制代码
    @Override
    public Object visit(ASTCompilationUnit node, Object data) {
      return super.visit(node, data);
    }

可以看到入参为:ASTCompilationUnit类型,对比ast语法树可以发现为根节点,以此为根节点向下遍历处理即可,同时

编辑

复制代码
net.sourceforge.pmd.lang.java.ast

该包下提供了大量的ast树节点,选择相应的节点树遍历即可。

同时,选择节点还可以通过xpath语法来获取ast树节点,例如:

编辑

通过xpath语法可以获取到相应的节点

同时xpath也支持正则表达式

具体写法可以看上方代码示例

规则写完之后要在xml中配置相应xpath规则指向

xml 复制代码
<rule name="TransactionMustHaveGlobalLockOrGlobalTransactional"
      language="java"
      since="1.6"
      message="java.exception.MethodTransactionMustHaveGlobalLockOrGlobalTransactionRule.msg"
      class="com.alibaba.p3c.pmd.lang.java.rule.exception.TransactionMustHaveGlobalLockOrGlobalTransactional">
    <priority>1</priority>
    <example>
        <![CDATA[
public int method() {
    Integer a = null;
    return a;
}
        ]]>
    </example>
</rule>

主要是配置name规则名,message指向,处理类class

example中可以填写正确规范的代码

同时message指向key为message.xml中的键

配置如下:

xml 复制代码
<entry key="java.exception.MethodTransactionMustHaveGlobalLockOrGlobalTransactionRule.msg">
    <![CDATA[方法【%s】本地事务注解未加全局事务@GlobalLock或全局锁@GlobalTransactional]]>
</entry>

发送消息可以使用:

javascript 复制代码
           addViolationWithMessage(data, methodNode,
                            "java.exception.MethodTransactionMustHaveGlobalLockOrGlobalTransactionRule.msg",
                            new Object[]{methodNode.getMethodName()});

指定相应的key和占位符替换内容

3、测试类书写

scala 复制代码
public class OopRuleTest extends SimpleAggregatorTst {

    // 加载CLASSPATH下的rulesets/java/ali-oop.xml
    private static final String RULESET = "java-ali-oop";

    @Override
    public void setUp() {
        addRule(RULESET, "EqualsAvoidNullRule");
        addRule(RULESET, "WrapperTypeEqualityRule");
        addRule(RULESET, "PojoNoDefaultValueRule");
        addRule(RULESET, "PojoMustUsePrimitiveFieldRule");
        addRule(RULESET, "PojoMustOverrideToStringRule");
        addRule(RULESET, "StringConcatRule");
        addRule(RULESET, "BigDecimalAvoidDoubleConstructorRule");
        addRule(RULESET, "AbstractMsunRule");
    }
}

通过继承SimpleAggregatorTst并使用addRule(RULESET, "AbstractMsunRule")添加测试规则

  • 添加测试数据:(在测试类的resources文件下的如图所示的xml中配置)

    编辑

xml 复制代码
<!--	示例代码-->
	<code-fragment id="class-without-comments"><![CDATA[
public class CommentsMustBeJavadocFormat {
    private String name;
    public void getName() {}
}
    ]]>
	</code-fragment>
	
	<test-code>
<!--		描述-->
		<description>Class have no comment.</description>
<!--		忽略 0 一个也不忽略-->
		<expected-problems>0</expected-problems>
<!--		对应的code-fragment id-->
		<code-ref id="class-without-comments" />
	</test-code>

4、打包

pom中添加如下配置,跳过测试和javadoc生成

vbnet 复制代码
<properties>
    <maven.test.skip>true</maven.test.skip>
    <maven.javadoc.skip>true</maven.javadoc.skip>
</properties>

mvn install 即可

相关推荐
To_OC3 天前
万字解析《JS 语言精粹》之第五章:继承 5 大核心精髓(JS 原型核心)
前端·javascript·代码规范
Coffeeee3 天前
闲聊几句,Android老哥们,你们多久没做技改需求了
android·程序员·代码规范
饼干哥哥3 天前
扣子3.0测评:我让 Codex 和 Claude Code 住同一个桌面,结果它们打架了!
人工智能·开源·代码规范
码哥字节5 天前
为什么 Claude Code 读你的代码库,光靠 embedding 根本不够?
claude·代码规范
kisshyshy7 天前
从递归到迭代,一文吃透二叉树的核心知识与 JavaScript 实现
javascript·算法·代码规范
用户69190268133910 天前
Vibe Coding 开发项目的基本范式
人工智能·设计模式·代码规范
Cosolar11 天前
藏在 Claude Code 里的极致浪漫:完整 187 条 Spinner Verbs 全收录
后端·程序员·代码规范
Mickey86112 天前
MCP 加持下的零代码逆向:全自动化绕过 APP 验签与加密实战
代码规范
专注VB编程开发20年15 天前
WebView2 + HostObject 架构的核心痛点 ——强耦合、同步阻塞、异常连锁、内核绑定
代码规范