Parsing Java annotations

At kaChing, we are on a 5-minute commit-to-production cycle. To achieve this extreme iteration cycle, we are running a series of very fast analyses on our code base. From discovering bad code snippets to detecting missing Guice‘s bindings, these tests strive to catch common mistakes due to distracted developers, new hires or bad APIs. As an engineering organization, we strongly believe that investing time writing tools to detect recurring problems automatically is worthwhile.

One of these tests, named the Dependency Test, detects unexpected dependencies between Java packages. The Dependency Test is configured by specifying the legal dependencies in a declarative fashion.

dependencies.forPackages(
    "com.kaching.*"
    ).
check("com.kaching.supermap").mayDependOn(
    "com.kaching.platform.guice",
    "voldemort.*"
    ).
assertIsVerified(jDepend.getPackages());

Relying on the JDepend library, the test traverses Java class file directories and records dependencies by inspecting the bytecode. It then compares the recorded dependencies with the specified ones and generates a report. If the dependencies do not match, our build goes red and no code is deployed to production until the issue is resolved.

In addition to detecting undesired dependencies between our packages, the Dependency Test turned out to be extremely useful to detect bad imports due to using auto completion too hastily. Unfortunately, JDepend ignores annotations. Assuming two classes named IsinType are in our classpath, a bad import in the following snippet would not be detected.

@Value(type = IsinType.class)
private Isin isin;

After two bad pushes due to a mistake of this kind, we decided to augment JDepend with annotation parsing. We quickly realized that the library doesn’t rely on ASM, which we know well, but implements its own bytecode parser.

The Java annotation specification is quite hairy, actually. For example, the following is a legal annotation. The @CodeSnippets annotation has an array of @Check annotations as its only element. @Check has two element-value pairs; one whose value is a String and one whose value is an array of @Snippets. Similarly, @Snippet is itself composed of two element-value pairs; one whose value is a String and one whose value is an array of Strings.

@CodeSnippets({
@Check(paths = "src/com/kaching", snippets = {
    // always using ET.toYearMonthDay
    @Snippet(value = "[^E][^T]\.toYearMonthDay\(", exceptions = {
        "src/com/kaching/util/time/TimeZone.java"
    }),
    // never call default super constructor
    @Snippet(value = "super\(\)")
})})

The specification defines an annotation as the following.

annotation {
    u2 type_index;
    u2 num_element_value_pairs;
    {
        u2 element_name_index;
        element_value value;
    } element_value_pairs[num_element_value_pairs]
}

type_index is an index into the constant pool table pointing to the type of the annotation. It is followed by the number of element-value pairs in the annotation. An element-value pair is composed of an index into the constant pool table pointing to the name of the element (e.g. value, snippets) and of its value. The value is defined as such:

element_value {
    u1 tag;
    union {
        u2 const_value_index;
        { 
            u2 type_name_index;
            u2 const_name_index;
        } enum_const_value;
        u2 class_info_index;
        annotation annotation_value;
        {
            u2 num_values;
            element_value values[num_values];
        } array_value;
    } value;
}

In English, it says that a value is either a constant (true, "src/com/kaching"), an enumeration (RetentionPolicy.RUNTIME), a class (IsinType.class), another annotation (@Check) or an array of values.

Our patch adds less than 100 lines of code (without considering tests), most of the work being done by the recursion. It is available will soon be available in the JDepend repository on github.

References