可以使用“ElementVisitor”来遍历方法主体中的语句吗?

Can an `ElementVisitor` be used to traverse the statements in the body of a method?

我正在尝试创建一个自定义注释,以检查是否在用它注释的方法主体中调用了某个方法。类似于:

@TypeQualifierDefault(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
@interface MutatingMethod {
}

interface Mutable {
  void preMutate();
  void postMutate();
  // other methods
}

然后在某个 Mutable class 内我们将有:

class Structure<T> implements Mutable {
  @MutatingMethod
  void add(T data) {
    preMutate();
    // actual mutation code
    postMutate();
  }
}

如果像 add 这样用 @MutatingMethod 注释的方法的主体 不包含 ,我希望能够得到某种警告调用 preMutatepostMutate。是否可以使用 ElementVisitor (javax.lang.model.element.ElementVisitor) 遍历方法主体中的(可能被混淆的)语句和方法调用?如果是这样,那会是什么样子?如果不行我还能用什么?

澄清一下,我知道这不可能(或更难)通过字节码反编译在运行时完成,因此此注释仅在编译期间起作用通过反射java.lang.reflect.*javax.lang.model.*),并且不会保留在 class 个文件中。

您可以随意修改代码,但是您希望它能正常工作,例如通过引入一个名为 @MutableType 的新注释,Structure 和任何其他 Mutable 类型必须对其进行注释以使其起作用。

最重要的是断言 preMutatepostMutate 之前而不是之后被调用。

没关系,但我正在使用 Gradle 和 IntelliJ IDEA IDE.

非常感谢任何帮助; material 这方面的内容出奇地少 and/or 网络上的不足。我一直在使用公开可用的资源来了解这一点!

注解处理器只处理声明,不处理字节码。它们不能用于对方法的内容进行断言。

如果始终调用这些方法对您很重要,您可能希望使用代理来强制执行此操作,而不是在每个方法中都使用样板代码。例如,您可以使用字节码工程库(如 Javassist)在运行时添加调用:

var f = new ProxyFactory();
f.setSuperclass(Foo.class);
f.setFilter(m -> m.isAnnotationPresent(MutatingMethod.class));
Class c = f.createClass();
Foo foo = c.newInstance();
((Proxy)foo).setHandler((self, m, proceed, args) -> {
    self.preMutate();
    proceed.invoke(self, args);
    self.postMutate();
});

foo.setName("Peter"); // automatically calls preMutate and postMutate()

(代码未经测试,因为我手头没有 IDE)

然后,只要您控制相关对象的创建(您可以通过使 super class abstract ).

有两个模块,

  • java.compiler which contains the API for annotation processors 以及您已经发现的简单抽象。

    ElementVisitor 抽象不支持挖掘方法的代码。

  • jdk.compiler模块,包含一个扩展的API,最初不被认为是标准的一部分API,因此不包含在官方API 引入模块系统之前的文档。

    这个API允许分析当前编译的源代码的语法树。

当您的起点是注释处理器时,您应该有一个 ProcessingEnvironment which was given to your init method. Then, you can invoke Trees.instance(ProcessingEnvironment) to get a helper object which has the method getTree(Element) 可用于获取语法树元素。然后,您可以从那里遍历语法树。

大多数 类 记录在 JDK 17 API 中确实已经存在于早期版本中(您可能会注意到“从 1.6 开始”),即使旧版本中不存在文档。但是在 JDK 9 之前,您必须在编译注释处理器时将特定 JDK 的 lib/tools.jar 包含到您的类路径中。

(编写模块化注释处理器时)
import javax.annotation.processing.Processor;

module anno.proc.example {
    requires jdk.compiler;
    provides Processor with anno.proc.example.MyProcessor;
}

package anno.proc.example;

import java.util.*;

import javax.annotation.processing.*;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;

import com.sun.source.tree.*;
import com.sun.source.tree.Tree.Kind;
import com.sun.source.util.Trees;

import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;

@SupportedSourceVersion(SourceVersion.RELEASE_17) // adapt when using older version
@SupportedAnnotationTypes(MyProcessor.ANNOTATION_NAME)
public class MyProcessor extends AbstractProcessor {
    static final String ANNOTATION_NAME = "my.example.MutatingMethod";
    static final String REQUIRED_FIRST = "preMutate", REQUIRED_LAST = "postMutate";

    // the inherited method does already store the processingEnv
    // public void init(ProcessingEnvironment processingEnv) {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Optional<? extends TypeElement> o = annotations.stream()
            .filter(e -> ANNOTATION_NAME.contentEquals(e.getQualifiedName())).findAny();
        if(!o.isPresent()) return false;
        TypeElement myAnnotation = o.get();

        roundEnv.getElementsAnnotatedWith(myAnnotation).forEach(this::check);

        return true;
    }

    private void check(Element e) {
        Trees trees = Trees.instance(processingEnv);
        Tree tree = trees.getTree(e);
        if(tree.getKind() != Kind.METHOD) { // should not happen as compiler handles @Target
            processingEnv.getMessager()
                .printMessage(Diagnostic.Kind.ERROR, ANNOTATION_NAME + " only allowed at methods", e);
            return;
        }
        MethodTree m = (MethodTree) tree;
        List<? extends StatementTree> statements = m.getBody().getStatements();
        if(statements.isEmpty() || !isRequiredFirst(statements.get(0))) {
            processingEnv.getMessager()
                .printMessage(Diagnostic.Kind.MANDATORY_WARNING,
                    "Mutating method does not start with " + REQUIRED_FIRST + "();", e);
        }
        // open challenges:
        //   - accept a return statement after postMutate();
        //   - allow a try { body } finally { postMutate(); }
        if(statements.isEmpty() || !isRequiredLast(statements.get(statements.size() - 1))) {
            processingEnv.getMessager()
                .printMessage(Diagnostic.Kind.MANDATORY_WARNING,
                    "Mutating method does not end with " + REQUIRED_LAST + "();", e);
        }
    }

    private boolean isRequiredFirst(StatementTree st) {
        return invokes(st, REQUIRED_FIRST);
    }

    private boolean isRequiredLast(StatementTree st) {
        return invokes(st, REQUIRED_LAST);
    }

    // check whether tree is an invocation of a no-arg method of the given name
    private boolean invokes(Tree tree, String method) {
        if(tree.getKind() != Kind.EXPRESSION_STATEMENT) return false;
        tree = ((ExpressionStatementTree)tree).getExpression();
        if(tree.getKind() != Kind.METHOD_INVOCATION) return false;

        MethodInvocationTree i = (MethodInvocationTree)tree;

        if(!i.getArguments().isEmpty()) return false; // not a no-arg method

        ExpressionTree ms = i.getMethodSelect();
        // TODO add support for explicit this.method()
        return ms.getKind() == Kind.IDENTIFIER
                && method.contentEquals(((IdentifierTree)ms).getName());
    }
}