Start with examples, but then write tests
When you write an Active Annotation, you will want quick feedback at first. So create an example project and write a small class that uses your annotation. Open the generated Java code of that class side by side with your annotation processor and see whether you are on the right track.
Once you are confident in the functionality however, it's time to write tests. Examples will only bring you so far and they always need to be in a different project. But you want test cases to be run in the same project that compiles your annotation processor. There is a section on testing active annotations in the Xtend Tutorial. I tend to use JNario and break down my tests into lots of little facts about the generated code. For an example, just look at the specification of the @Cached annotation.
Do not rename things
This is probably the most important advice. Renaming things will break reflection based frameworks and other annotation processors that come after you.
Sadly, the two annotations shipped with Xtend, @Property and @Data, do just that. If you have a field called "firstName", they will generate an accessor method called "getFirstName", but the field will be renamed to "_firstName". This breaks frameworks like JAXB or Bean Validation. It also makes the code less clear because you define a field and then access it under a different name. Never do this!
The more advanced problem is composability. If there are several annotation processors on a class and the first one renames a couple of fields, then the subsequent processor will produce bogus results.
Don't overwrite handwritten code
This is a more advanced technique that will make your users smile. Let's say your annotation can automatically generate a toString() method for a class. This is nice, but sometimes users will have an implementation that prints something more meaningful than your default implementation. If you just blindly add the toString() method, then they will get a "duplicate method" exception when trying to override it. So check if there already is a toString() method and only if there isn't you may add your default one.
xtend-contrib-base has utility methods that allow you to easily find out whether a class already has a certain method. Plus, there are convenient shortcuts for commonly used methods like equals, hashCode and toString.
Extract commonly used transformations
There are transformations that occur over and over again. For instance, you may want to implement a method from a given interface. This is pretty much always the same, copying the signature from the interface and then adding a body.
Other common transformations are:
- adding a toString() method that uses all persistent fields
- adding a hashCode() method that uses all persistent fields
- adding an equals() method that uses all persistent fields
- adding a constructor that takes an argument for each persistent field
- wrapping a method (see below)
Move these out into a helper class so you don't introduce copy-paste errors. xtend-contrib-base has methods for all the cases described above and more.
Pattern: Wrapping a method
Many method level annotations want to add some behavior around the actual body of the original method. This can be achieved as follows:
- Add a new private method with exactly the same signature as the original one and give it a special name
- Move the body of the original method to the new private method
- Replace the body of the original method with a call to the new method, adding behavior before and after as needed
In future Xtend versions, you will be able to just alter the body itself and add statements to it. But as of now, the pattern described above works very well. Of course, xtend-contrib-base has a convenient function that will do just that for you. All you have to specify are the name of the inner method and the new body of the outer method.
Design for composition or document bad composability
Active Annotations deeply manipulate the AST of a class. This makes them pretty hard to combine, as one annotation might stumble over changes that another has already made. If you want to make your annotation as composable as possible you need to think hard about how you can avoid interference with other ones.
The "do not rename things" advice is one of the most important steps. But you also need to think about whether you want to process members that other annotations have generated before you or if you want to ignore them. It depends on the semantics of your annotation. The TransformationContext has an "isGenerated" method that will tell you whether something was created by another active annotation.
If you are having a hard time getting this right, don't feel bad. It's already hard in relatively simple cases and almost impossible for complex annotations. In that case, just document the fact that your annotation is not safe to use with others.
Add helpful error messages
If your annotation cannot do its job, do not just fail with NullPointer- or ClassCastExceptions. Add helpful error messages to the offending points in the code. This will help your users learn how to use your annotation much faster.
Make it quick
Your annotation processor will be run on each incremental compiler run. This means each time the user types something! It needs to be fast or your users will be put off. So avoid I/O and other heavyweight tasks as much as possible!
One idea I often hear is generating DDL. Don't do this with active annotations!
First off, you probably do not need your "Create Table" statements to be up to date on each change to a source file. Second, there are existing tools for that. Third, even if no tool suits your need, you should just use runtime reflection to collect the data from your compiled classes and then generate the DDL from that. This makes the generator much easier (the active annotation API is pretty low level!). Plus, you can run it as a separate, one-time build step after compilation. So instead of directly generating SQL files, just add the mapping annotations to your classes and then use existing tools from there.
If it can be done with a framework, use it
A last word of caution: Active annotations are cool, but there are many problems that can be solved by using a framework. They are usually much more composable than class manipulation, so leave active annotations for cases where you actually want to see and directly call the generated code.
ORM is a good counter-example here. You don't want to see the synthetic methods that take care of lazy loading, dirty checking etc. Those are implementation details that best stay hidden. If one of your frameworks forces you to write lots of boilerplate, be sure to look for better frameworks before writing a boilerplate-generating annotation.