For example, I wanted to create the annotation @Out to target parameters. Then I would somehow use the compiler to check if the parameter value is set before the function return
The javac compiler supports user-definable plugins, called annotation processors, that accomplish exactly what you want. You can think of annotations as language extensions.
The definition public @interface Immutable { ... }
defines the syntax: the @Immutable
annotation that you can write in your program. The annotation processor (the compiler plug-in) defines the semantics: it enforces the semantic rules and issues compiler warnings when your program violates the rules.
One framework that makes it easy to write such annotation processors is the Checker Framework, and it contains definitions for annotations like @NonNull
and @Immutable
. Here are two tutorials about how to use the Checker Framework to validate code: tutorial 1, tutorial 2.
Ordinary Java annotation processing is invoked on each declaration, such as classes, fields, methods, and method parameters, and ordinary Java gives the annotation processor no access to the program's full AST. You can think of the Checker Framework as a library that extends the power of Java annotation processing. It gives you access to the full AST of each class, and it lets you define rules for every statement in your program. Thus, your annotation processor can issue warnings when a statement invokes a non-@Const
method on an @Immutable
object.
Your annotation processor should be modular, working one class at a time. The annotation processor has access to the AST of the current class, plus the signatures, including annotations, of all classes that it uses. Annotation processing gives you that information (but not to the whole project's AST all at once).