3

Let's consider the following code:

switch ( <em>switchTreeExpression</em> ) {
    <em>cases</em>
}

I want to find out, what type for switchTreeExpression is .

I have the following code draft:

...
MethodTree methodTree = trees.getTree(method);
BlockTree blockTree = methodTree.getBody();

for (StatementTree statementTree : blockTree.getStatements()) {
    if (statementTree.getKind() == Tree.Kind.SWITCH) {
        SwitchTree switchTree = (SwitchTree) statementTree;
        ExpressionTree switchTreeExpression = switchTree.getExpression();
        // I need to get the type of *switchTreeExpression* here
    }
}

It is interesting, that I can get the type of switchTreeExpression from .class file. However it seems that there is no way to get byte code of the current class in this phase of annotation processing (if I am wrong, I would be happy just get byte code and analyze it with ObjectWeb ASM library).

Denis
  • 3,595
  • 12
  • 52
  • 86
  • 2
    There is nothing special about the expression of a `switch`. You get its type the same way as for any other expression. If that’s your actual question, how to get the type of an `ExpressionTree` in general, look at [`Trees.getTypeMirror(TreePath)`](https://docs.oracle.com/en/java/javase/17/docs/api/jdk.compiler/com/sun/source/util/Trees.html#getTypeMirror(com.sun.source.util.TreePath)). – Holger Jun 27 '22 at 08:13
  • @Holger thanks for the response. Sure, let's say that I want to get a type of some expression in general. I saw `getTypeMirror` before, but do you know, how to get `TreePath` for some expression? For example, for `switchTreeExpression` in my case? – Denis Jun 27 '22 at 09:05
  • 3
    The simplest is [`TreePath.getPath(compilationUnit, switchTreeExpression)`](https://docs.oracle.com/en/java/javase/17/docs/api/jdk.compiler/com/sun/source/util/TreePath.html#getPath(com.sun.source.tree.CompilationUnitTree,com.sun.source.tree.Tree)), I suppose. – Holger Jun 27 '22 at 09:09
  • @Holger yep, I saw this method as well, but I don't know how to get `compilationUnit`. – Denis Jun 27 '22 at 09:29
  • 1
    What is your starting point? When your first line is [calling this method](https://docs.oracle.com/en/java/javase/17/docs/api/jdk.compiler/com/sun/source/util/Trees.html#getTree(javax.lang.model.element.ExecutableElement)), then you can also call [that method](https://docs.oracle.com/en/java/javase/17/docs/api/jdk.compiler/com/sun/source/util/Trees.html#getPath(javax.lang.model.element.Element)) and [query the resulting path](https://docs.oracle.com/en/java/javase/17/docs/api/jdk.compiler/com/sun/source/util/TreePath.html#getCompilationUnit()) – Holger Jun 27 '22 at 09:35
  • @Holger I got a `compilationUnit` and tried to execute `TreePath.getPath()` for a few different `Tree target`. For each invocation I received valid `TreePath`. However, when I executed `trees.getTypeMirror()` for the `TreePath`, I always got `null`. I've tried the following `tree target`: `switchTreeExpression`, `(ParenthesizedTree) switchTreeExpression`, and `(IdentifierTree) ((ParenthesizedTree) switchTreeExpression).getExpression()` – Denis Jun 27 '22 at 22:45
  • Dear @Denis, Could you please provide a [minimal, complete, and verifiable example](https://stackoverflow.com/help/mcve)? – Sergey Vyacheslavovich Brunov Jul 01 '22 at 09:00
  • Dear @Denis, Additionally, could you please provide more context on what you are trying to accomplish (the goal)? Why would you like to have an annotation processor instead of, for example, a javac plugin or a stand-alone program that uses javac functionality? – Sergey Vyacheslavovich Brunov Jul 03 '22 at 10:46
  • Dear @Denis, Would you like to check all methods or, for example, only annotated methods (annotated with a custom annotation)? – Sergey Vyacheslavovich Brunov Jul 03 '22 at 11:12
  • 1
    @Sergey Vyacheslavovich Brunov, I want to test switch exhaustiveness for Enums in methods/classes, which are annotated by user. Unfortunately, `javac` adds fake switch cases for switch in some conditions. So, when `javac` finishes compilation, there is no way to find out, whether user covers all branches, or not (for some cases). https://github.com/openjdk/jdk/blob/master/src/java.xml/share/classes/com/sun/org/apache/bcel/internal/generic/SWITCH.java#L85 That's why I decided to use annotation processor. This API works before inserting fake branches. – Denis Jul 03 '22 at 11:14
  • @SergeyVyacheslavovichBrunov check this example https://pastebin.com/mKVJhh81 We have different java code, but the same byte code after compilation. So, it seems I cannot use anything except annotation processors – Denis Jul 03 '22 at 11:23

1 Answers1

2

Possible solutions

Annotation processor

Let's consider an annotation processor for the type annotations (@Target(ElementType.TYPE)).

Limitation: Processor.process() method: No method bodies

Processing Code:

Annotation processing occurs at a specific point in the timeline of a compilation, after all source files and classes specified on the command line have been read, and analyzed for the types and members they contain, but before the contents of any method bodies have been analyzed.

Overcoming limitation: Using com.sun.source.util.TaskListener

The idea is to handle the type element analysis completion events.

  • Processor.init() method: Register a task listener and handle the type element analysis completion events using the captured annotated type elements.
  • Processor.process() method: Capture the annotated type elements.

Some related references:

Note on implementation approaches

Some third-party dependencies (libraries and frameworks) may be used to implement an annotation processor.

For example, the already mentioned Checker Framework.

Some related references:

Please, note that the Checker Framework processors use @SupportedAnnotationTypes("*").

Draft implementation

Let's consider a draft implementation, which does not use third-party dependencies mentioned in the «Note on implementation approaches» section.

Annotation processor project

Maven project
<properties>
    <auto-service.version>1.0.1</auto-service.version>
</properties>
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
        <annotationProcessorPaths>
            <path>
                <groupId>com.google.auto.service</groupId>
                <artifactId>auto-service</artifactId>
                <version>${auto-service.version}</version>
            </path>
        </annotationProcessorPaths>
    </configuration>
</plugin>
<dependency>
    <groupId>com.google.auto.service</groupId>
    <artifactId>auto-service-annotations</artifactId>
    <version>${auto-service.version}</version>
</dependency>
AbstractTypeProcessor class: Base class

Let's introduce the base class that has the following abstract method:

public abstract void processType(Trees trees, TypeElement typeElement, TreePath treePath);
import com.sun.source.util.JavacTask;
import com.sun.source.util.TaskEvent;
import com.sun.source.util.TaskListener;
import com.sun.source.util.TreePath;
import com.sun.source.util.Trees;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.ElementFilter;

// NOTE: It is designed to work only with `@Target(ElementType.TYPE)` annotations!
public abstract class AbstractTypeProcessor extends AbstractProcessor {
    private final AnalyzeTaskListener analyzeTaskListener = new AnalyzeTaskListener(this);
    protected final Set<Name> remainingTypeElementNames = new HashSet<>();
    private Trees trees;

    protected AbstractTypeProcessor() {
    }

    @Override
    public synchronized void init(final ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        trees = Trees.instance(processingEnv);
        JavacTask.instance(processingEnv).addTaskListener(analyzeTaskListener);
    }

    @Override
    public boolean process(final Set<? extends TypeElement> annotations, final RoundEnvironment roundEnv) {
        for (final TypeElement annotation : annotations) {
            final Set<? extends Element> annotatedElements = roundEnv.getElementsAnnotatedWith(annotation);
            final Set<TypeElement> typeElements = ElementFilter.typesIn(annotatedElements);
            final List<Name> typeElementNames = typeElements.stream()
                .map(TypeElement::getQualifiedName)
                .toList();
            remainingTypeElementNames.addAll(typeElementNames);
        }
        System.out.println(
            String.format("Remaining type element names: %s", remainingTypeElementNames)
        );
        return false;
    }

    public abstract void processType(Trees trees, TypeElement typeElement, TreePath treePath);

    private void handleAnalyzedType(final TypeElement typeElement) {
        System.out.println(
            String.format("Handling analyzed type element: %s", typeElement)
        );
        if (!remainingTypeElementNames.remove(typeElement.getQualifiedName())) {
            return;
        }

        final TreePath treePath = trees.getPath(typeElement);
        processType(trees, typeElement, treePath);
    }

    private static final class AnalyzeTaskListener implements TaskListener {
        private final AbstractTypeProcessor processor;

        public AnalyzeTaskListener(final AbstractTypeProcessor processor) {
            this.processor = processor;
        }

        @Override
        public void finished(final TaskEvent e) {
            if (e.getKind() != TaskEvent.Kind.ANALYZE) {
                return;
            }

            processor.handleAnalyzedType(e.getTypeElement());
        }
    }
}
CheckMethodBodies class: Annotation class
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface CheckMethodBodies {
}
CheckMethodBodiesProcessor class: Annotation processor
import com.google.auto.service.AutoService;
import com.sun.source.tree.BlockTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.StatementTree;
import com.sun.source.tree.SwitchTree;
import com.sun.source.tree.Tree;
import com.sun.source.util.TreePath;
import com.sun.source.util.TreePathScanner;
import com.sun.source.util.Trees;
import javax.annotation.processing.Processor;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;

@SupportedAnnotationTypes("org.example.annotation.processor.CheckMethodBodies")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@AutoService(Processor.class)
public final class CheckMethodBodiesProcessor extends AbstractTypeProcessor {
    @Override
    public void processType(final Trees trees, final TypeElement typeElement, final TreePath treePath) {
        final CompilationUnitTree compilationUnitTree = treePath.getCompilationUnit();
        final TestMethodTreePathScanner treePathScanner = new TestMethodTreePathScanner(trees, compilationUnitTree);
        treePathScanner.scan(compilationUnitTree, null);
    }

    private static final class TestMethodTreePathScanner extends TreePathScanner<Void, Void> {
        private final Trees trees;
        private final CompilationUnitTree compilationUnitTree;

        public TestMethodTreePathScanner(
            final Trees trees,
            final CompilationUnitTree compilationUnitTree
        ) {
            this.trees = trees;
            this.compilationUnitTree = compilationUnitTree;
        }

        @Override
        public Void visitMethod(final MethodTree node, final Void unused) {
            System.out.println(
                String.format("Visiting method: %s", node.getName())
            );

            final BlockTree blockTree = node.getBody();
            for (final StatementTree statementTree : blockTree.getStatements()) {
                if (statementTree.getKind() != Tree.Kind.SWITCH) {
                    continue;
                }

                final SwitchTree switchTree = (SwitchTree) statementTree;
                final ExpressionTree switchTreeExpression = switchTree.getExpression();
                System.out.println(
                    String.format("Switch tree expression: %s", switchTreeExpression)
                );

                final TreePath treePath = TreePath.getPath(compilationUnitTree, switchTreeExpression);
                final TypeMirror typeMirror = trees.getTypeMirror(treePath);
                System.out.println(
                    String.format("Tree mirror: %s", typeMirror)
                );
            }
            return null;
        }
    }
}

Test project

Maven project
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
        <annotationProcessorPaths>
            <path>
                <groupId>org.example</groupId>
                <artifactId>annotation-processor</artifactId>
                <version>1.0.0-SNAPSHOT</version>
            </path>
        </annotationProcessorPaths>
        <showWarnings>true</showWarnings>
    </configuration>
</plugin>

To be able to use the annotation class:

<dependency>
    <groupId>org.example</groupId>
    <artifactId>annotation-processor</artifactId>
    <version>1.0.0-SNAPSHOT</version>
</dependency>
Switcher class: Using annotation
import org.example.annotation.processor.CheckMethodBodies;

@CheckMethodBodies
public final class Switcher {
    public void theMethod() {
        final Integer value = 1;
        switch (value.toString() + "0" + "0") {
            case "100":
                System.out.println("Hundred!");
            default:
                System.out.println("Not hundred!");
        }
    }
}

Testing

Execute the command for the annotation processor project:

mvn clean install

Execute the command for the test project:

mvn clean compile

Observe the output of the annotation processor:

Remaining type element names: [org.example.annotation.processor.test.Switcher]
Remaining type element names: [org.example.annotation.processor.test.Switcher]
Handling analyzed type element: org.example.annotation.processor.test.Switcher
Visiting method: <init>
Visiting method: theMethod
Switch tree expression: (value.toString() + "00")
Tree mirror: java.lang.String

Stand-alone program

It is possible to use javac functionality in a stand-alone program.

It seems that it is necessary to get the tree path and then get the type mirror:

final CompilationUnitTree compilationUnitTree = <…>;
final ExpressionTree switchTreeExpression = <…>;

final TreePath treePath = TreePath.getPath(compilationUnitTree, switchTreeExpression);
final TypeMirror typeMirror = trees.getTypeMirror(treePath);

An excerpt from the documentation: TypeMirror (Java Platform SE 8 ):

public interface TypeMirror extends AnnotatedConstruct

Represents a type in the Java programming language. Types include primitive types, declared types (class and interface types), array types, type variables, and the null type. Also represented are wildcard type arguments, the signature and return types of executables, and pseudo-types corresponding to packages and to the keyword void.

Draft implementation

Input file: Switcher class

public final class Switcher {
    public void theMethod() {
        final Integer value = 1;
        switch (value.toString() + "0" + "0") {
            case "100":
                System.out.println("Hundred!");
            default:
                System.out.println("Not hundred!");
        }
    }
}

Program class

Please, replace the "/path/to/Switcher.java" file path value with the actual file path value.

import com.sun.source.tree.BlockTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.StatementTree;
import com.sun.source.tree.SwitchTree;
import com.sun.source.tree.Tree;
import com.sun.source.util.JavacTask;
import com.sun.source.util.TreePath;
import com.sun.source.util.TreePathScanner;
import com.sun.source.util.Trees;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import javax.lang.model.type.TypeMirror;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import javax.tools.ToolProvider;

public final class Program {
    public static void main(final String[] args) throws IOException {
        final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        final JavacTask task = (JavacTask) compiler.getTask(
            null,
            null,
            null,
            null,
            null,
            List.of(new TestFileObject())
        );
        final Iterable<? extends CompilationUnitTree> compilationUnitTrees = task.parse();
        task.analyze();
        final Trees trees = Trees.instance(task);

        for (final CompilationUnitTree compilationUnitTree : compilationUnitTrees) {
            final TestMethodTreePathScanner treePathScanner = new TestMethodTreePathScanner(trees, compilationUnitTree);
            treePathScanner.scan(compilationUnitTree, null);
        }
    }

    private static final class TestFileObject extends SimpleJavaFileObject {
        public TestFileObject() {
            super(URI.create("myfo:/Switcher.java"), JavaFileObject.Kind.SOURCE);
        }

        @Override
        public CharSequence getCharContent(final boolean ignoreEncodingErrors) throws IOException {
            return Files.readString(
                Path.of("/path/to/Switcher.java"),
                StandardCharsets.UTF_8
            );
        }
    }

    private static final class TestMethodTreePathScanner extends TreePathScanner<Void, Void> {
        private final Trees trees;
        private final CompilationUnitTree compilationUnitTree;

        public TestMethodTreePathScanner(
            final Trees trees,
            final CompilationUnitTree compilationUnitTree
        ) {
            this.trees = trees;
            this.compilationUnitTree = compilationUnitTree;
        }

        @Override
        public Void visitMethod(final MethodTree node, final Void unused) {
            final BlockTree blockTree = node.getBody();
            for (final StatementTree statementTree : blockTree.getStatements()) {
                if (statementTree.getKind() != Tree.Kind.SWITCH) {
                    continue;
                }

                final SwitchTree switchTree = (SwitchTree) statementTree;
                final ExpressionTree switchTreeExpression = switchTree.getExpression();
                System.out.println(
                    String.format("Switch tree expression: %s", switchTreeExpression)
                );

                final TreePath treePath = TreePath.getPath(compilationUnitTree, switchTreeExpression);
                final TypeMirror typeMirror = trees.getTypeMirror(treePath);
                System.out.println(
                    String.format("Tree mirror: %s", typeMirror)
                );
            }
            return null;
        }
    }
}

The program output:

Switch tree expression: (value.toString() + "00")
Tree mirror: java.lang.String
  • 1
    thanks for the response. So, you use Java compiler, but not annotation processor, right? I mean, maybe I could also start using it, but it is not the same. Or you suspect, that you approach should work for annotation processors as well? – Denis Jul 01 '22 at 13:12
  • Dear @Denis, «So, you use Java compiler, but not annotation processor, right?» — Yes. It is a stand-alone program that uses javac functionality. «I mean, maybe I could also start using it, but it is not the same.» — Yes, they are not the same. – Sergey Vyacheslavovich Brunov Jul 03 '22 at 11:02
  • Dear @Denis, «Or you suspect, that you approach should work for annotation processors as well?» — Yes, exactly. I suspected this. But after checking the approach with an annotation processor in a straightforward way, it turned out that this is not the case: getting the null reference as the return value of the `com.sun.source.util.Trees.getTypeMirror()` method call. – Sergey Vyacheslavovich Brunov Jul 03 '22 at 11:02
  • yes, exactly, It is a bit strange, because theses APIs looks similar. – Denis Jul 03 '22 at 11:09
  • Dear @Denis, Please, see the updated answer. – Sergey Vyacheslavovich Brunov Jul 03 '22 at 12:52
  • Dear @Denis, Corrected the package name. Please, see the updated answer. – Sergey Vyacheslavovich Brunov Jul 03 '22 at 13:00
  • 1
    thank you for helping, I will try these options. – Denis Jul 03 '22 at 13:01
  • Dear @Denis, Thank you for accepting the answer! Could you please let me know your result (whether the draft implementation of the annotation processor has worked for you)? – Sergey Vyacheslavovich Brunov Jul 04 '22 at 13:40
  • 1
    sure, I think I will have some draft this week. I will post there github link – Denis Jul 04 '22 at 14:38
  • 1
    I've pushed some draft of a code - https://github.com/Hixon10/switch-exhaustiveness-checker/tree/main/src/main/java/ru/hixon/switchexhaustivenesschecker I tried different tests locally, and everything works fine. Thank you one more time. – Denis Jul 05 '22 at 00:16
  • 1
    Dear @Denis, Thank you for the update! Glad to hear that it has helped you. Thank you for the collaboration! – Sergey Vyacheslavovich Brunov Jul 05 '22 at 09:01
  • 1
    Dear @Denis, Just for your information. I have updated the answer **to highlight** that some third-party dependencies (libraries and frameworks) may be used to implement an annotation processor instead of implementing it from «scratch» (as described in the «Draft implementation» section). Please, see introduced «Note on implementation approaches» section. – Sergey Vyacheslavovich Brunov Jul 05 '22 at 09:39