PAYATHON 2026

PMD XPath 规则如何判断两个 ClassType 节点是否表示同一个 Java 类?

技术老齐

结论

PMD 7.24.0 的内置 XPath 接口没有通用方法来获取任意 ClassType 节点解析后的 FQCN,也没有内置函数可直接比较两个节点所代表的类型。

需要比较任意两个类时,建议编写 Java 规则:通过 getTypeMirror() 获取类型,再比较类符号。这样无需预先知道 FQCN,比较结果也不受简单类名、导入方式或全限定名称写法的影响。

目标类已知时,可以继续使用 XPath,通过 pmd-java:typeIsExactly 判断是否为指定类。PMD 7.24.0 XPath 文档

为什么 Designer 能显示,XPath 却读不到?

Designer 显示的类型信息与 XPath 能访问的属性并不完全相同。PMD 只将部分 Java getter 暴露为 XPath 属性。即使界面显示了 Type: java.lang.String,也不能据此认定存在 @Type 或 @TypeMirror 属性。XPath 的 AST 映射说明

ClassType 的名称属性也不能替代类型解析:

  • @SimpleName 只有简单名称,无法区分 java.util.Date 和 java.sql.Date。
  • @PackageQualifier 反映源码中的包限定部分。类以简单名称引用时,这个属性无法补出包名。

因此,拼接这两个属性无法得到适用于所有引用方式的 FQCN。具体行为可参阅 ASTClassType 源码。

已知目标类时,使用 typeIsExactly

题目中的 String 示例可以直接写成:

//ClassType[pmd-java:typeIsExactly('java.lang.String')]

只要类型解析正确,String 和 java.lang.String 两种写法都会匹配。

typeIs 接受目标类型及其子类型,typeIsExactly 则检查是否为指定类型。例如,ArrayList 可以满足 typeIs('java.util.List'),但它和 List 并不是同一个类或接口。类型检查函数说明

任意两个类的比较,放到 Java 规则中处理

  1. 根据规则的业务条件,找到需要比较的两个 ASTClassType 节点。
  2. 调用 getTypeMirror(),确认解析结果是 JClassType。
  3. 获取各自的 JClassSymbol,使用 equals 比较。

Java 规则通过 TypeNode.getTypeMirror() 访问类型信息,再通过 JClassType.getSymbol() 获取声明该类型的类符号。Java 类型解析 API、JClassType 接口

下面的辅助类可用于 Java 自定义规则:

import net.sourceforge.pmd.lang.java.ast.ASTClassType;
import net.sourceforge.pmd.lang.java.symbols.JClassSymbol;
import net.sourceforge.pmd.lang.java.types.JClassType;
import net.sourceforge.pmd.lang.java.types.JTypeMirror;

public final class ClassTypeComparison {

    private ClassTypeComparison() {
    }

    private static JClassSymbol resolvedClass(ASTClassType node) {
        JTypeMirror type = node.getTypeMirror();
        if (!(type instanceof JClassType)) {
            return null;
        }

        JClassSymbol symbol = ((JClassType) type).getSymbol();
        return symbol.isUnresolved() ? null : symbol;
    }

    public static boolean sameClass(ASTClassType left,
                                    ASTClassType right) {
        JClassSymbol leftSymbol = resolvedClass(left);
        JClassSymbol rightSymbol = resolvedClass(right);

        return leftSymbol != null
            && rightSymbol != null
            && leftSymbol.equals(rightSymbol);
    }

    public static String canonicalName(ASTClassType node) {
        JClassSymbol symbol = resolvedClass(node);
        return symbol == null ? null : symbol.getCanonicalName();
    }
}

如果传入题目中两个字段的 ClassType 节点,且类型解析成功,sameClass 应返回 true,对两个节点调用 canonicalName 都应得到 java.lang.String。实际规则仍需自行选择节点并报告违规,这些辅助方法只负责比较类和获取类名。

判断是否为同一个类时,直接比较符号即可;需要输出类名时,再调用 getCanonicalName()。该方法允许返回 null,例如局部类就没有 canonical name。JClassSymbol 接口

注意“同一个类”和“同一个类型”的区别

sameClass 忽略泛型实参。例如:

List<String> names;
List<Integer> counts;

这两个类型对应同一个 java.util.List 类符号,因此 sameClass 返回 true。如果规则还要求泛型实参一致,应比较类型镜像:

left.getTypeMirror().equals(right.getTypeMirror())

PMD 的 JTypeMirror.equals 用于比较类型是否等价。不应使用 ==,也不应比较 getTypeMirror().toString(),因为后者是调试表示,格式可能变化。JTypeMirror 接口说明

分析所需的依赖还应放入 auxClasspath。示例对未解析符号采用保守处理:返回 false 只表示尚未确认两者相同,不能据此认定它们一定不同。PMD 可能为缺失依赖保留部分名称信息,实际规则需要明确如何处理这类情况。辅助类路径配置、未解析符号说明

备注:内容仅供参考。