Sam Harwell
Sam Harwell

Reputation: 99889

Access constant field in annotation processor

Suppose a class defines a constant field:

public class Foo {
  public static final int CONSTANT_FIELD = 3;
}

And suppose an annotation interface is declared like the following:

public @interface Something {
  int value();
}

Finally, suppose the annotation is used as follows:

@Something(Foo.CONSTANT_FIELD)

Question: In an annotation processor, how can I get the element for CONSTANT_FIELD from its use in setting the value of @Something?


Edit: Including a concrete example in the question itself.

I have an annotation that gets used like this:

@RuleDependency(recognizer = BQLParser.class,
                rule = BQLParser.RULE_statement,
                version = 0)

The annotation processor needs to know that RULE_statement is a constant defined in the BQLParser class. If I could access the Element for BQLParser.RULE_statement directly from setting the rule property of the annotation, it would eliminate the need for the recognizer property. This annotation is used thousands of times within real applications, and the recognizer is always just the declaring type of the rule constant. Resolving this question would simplify the annotation usage to just this:

@RuleDependency(rule = BQLParser.RULE_statement, version = 0)

Upvotes: 9

Views: 1431

Answers (2)

Sam Harwell
Sam Harwell

Reputation: 99889

I was able to implement this feature using the Compiler Trees API.

  1. Update the pom.xml to include the following profiles to make sure tools.jar is referenced on systems where it is not referenced by default.

    <profiles>
        <profile>
            <!-- Java 6 and earlier have java.vendor set to "Sun Microsystems Inc." -->
            <id>default-tools-6.jar</id>
            <activation>
                <property>
                    <name>java.vendor</name>
                    <value>Sun Microsystems Inc.</value>
                </property>
            </activation>
            <dependencies>
                <dependency>
                    <groupId>com.sun</groupId>
                    <artifactId>tools</artifactId>
                    <version>1.6</version>
                    <scope>system</scope>
                    <systemPath>${java.home}/../lib/tools.jar</systemPath>
                </dependency>
            </dependencies>
        </profile>
    
        <profile>
            <!-- Java 7 and later have java.vendor set to "Oracle Corporation" -->
            <id>default-tools.jar</id>
            <activation>
                <property>
                    <name>java.vendor</name>
                    <value>Oracle Corporation</value>
                </property>
            </activation>
            <dependencies>
                <dependency>
                    <groupId>com.sun</groupId>
                    <artifactId>tools</artifactId>
                    <version>1.6</version>
                    <scope>system</scope>
                    <systemPath>${java.home}/../lib/tools.jar</systemPath>
                </dependency>
            </dependencies>
        </profile>
    </profiles>
    
  2. Override Processor.init to get an instance of Trees.

    @Override
    public void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        this.trees = Trees.instance(processingEnv);
    }
    
  3. Implement a TreePathScanner<TypeMirror, Void>, which is used to obtain the TypeMirror of the declaring type assigned to the rule property in an annotation.

    private class AnnotationVisitor extends TreePathScanner<TypeMirror, Void> {
        @Override
        public TypeMirror visitAnnotation(AnnotationTree node, Void p) {
            for (ExpressionTree expressionTree : node.getArguments()) {
                if (!(expressionTree instanceof AssignmentTree)) {
                    continue;
                }
    
                AssignmentTree assignmentTree = (AssignmentTree)expressionTree;
                ExpressionTree variable = assignmentTree.getVariable();
                if (!(variable instanceof IdentifierTree) || !((IdentifierTree)variable).getName().contentEquals("rule")) {
                    continue;
                }
    
                return scan(expressionTree, p);
            }
    
            return null;
        }
    
        @Override
        public TypeMirror visitAssignment(AssignmentTree at, Void p) {
            return scan(at.getExpression(), p);
        }
    
        @Override
        public TypeMirror visitMemberSelect(MemberSelectTree mst, Void p) {
            return scan(mst.getExpression(), p);
        }
    
        @Override
        public TypeMirror visitIdentifier(IdentifierTree it, Void p) {
            return trees.getTypeMirror(this.getCurrentPath());
        }
    }
    
  4. Provide a default value for the recognizer property. I wish this could be null but Java explicitly forbids that...

    /**
     * Gets the recognizer class where the dependent parser rules are defined.
     * This may reference the generated parser class directly, or for simplicity
     * in certain cases, any class derived from it.
     * <p>
     * If this value is not specified, the default value {@link Parser}
     * indicates that the declaring type of the constant value specified for
     * {@link #rule} should be used as the recognizer type.
     * </p>
     */
    Class<? extends Recognizer<?, ?>> recognizer() default Parser.class;
    
  5. Update the code that gathers information about about the RuleDependency annotations applied to particular Element instances in the code to first try accessing the recognizer property, and if it's not specified, use the declaring type of the constant in the rule property instead. Error handling is omitted from this code sample for brevity.

    RuleDependency dependency = element.getAnnotation(RuleDependency.class);
    
    // first try to get the parser type from the annotation
    TypeMirror recognizerType = getRecognizerType(dependency);
    if (recognizerType != null && !recognizerType.toString().equals(Parser.class.getName())) {
        result.add(new Triple<RuleDependency, TypeMirror, Element>(dependency, recognizerType, element));
        continue;
    }
    
    // fallback to compiler tree API
    AnnotationMirror annotationMirror = null;
    for (AnnotationMirror mirror : element.getAnnotationMirrors()) {
        if (processingEnv.getTypeUtils().isSameType(ruleDependencyTypeElement.asType(), mirror.getAnnotationType())) {
            annotationMirror = mirror;
            break;
        }
    }
    
    AnnotationValue annotationValue = null;
    for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : annotationMirror.getElementValues().entrySet()) {
        if (entry.getKey().getSimpleName().contentEquals("rule")) {
            annotationValue = entry.getValue();
            break;
        }
    }
    
    TreePath treePath = trees.getPath(element, annotationMirror, annotationValue);
    AnnotationVisitor visitor = new AnnotationVisitor();
    recognizerType = visitor.scan(treePath, null);
    
    result.add(new Triple<RuleDependency, TypeMirror, Element>(dependency, recognizerType, element));
    

Upvotes: 3

bcody
bcody

Reputation: 2548

If rule is an int, it's not going to be possible for the annotation processor to find out where that int was defined. However, you could use an enum instead of an int for the rule field and group your rules in there with references to their parsers. Maybe something like this:

Parser interface:

public interface RuleParser {
}

Example implementation:

public class RuleParserImpl implements RuleParser {
}

Rule enum:

public enum Rule {

    RULE_STATEMENT(RuleParserImpl.class);

    private final Class<? extends RuleParser> ruleParserClass;

    private Rule(Class<? extends RuleParser> ruleParser) {
        this.ruleParserClass = ruleParser;
    }

    public Class<? extends RuleParser> getRuleParserClass() {
        return ruleParserClass;
    }
}

Annotation with enum instead of int field:

@Retention(RetentionPolicy.RUNTIME)
public @interface RuleDependency {
    Rule rule();
}

Usage example:

@RuleDependency(rule = Rule.RULE_STATEMENT)
public class RuleProcessor {

    public static void main(String[] args) {
        RuleDependency ruleDependency = RuleProcessor.class.getAnnotation(RuleDependency.class);
        Rule rule = ruleDependency.rule();
        Class<? extends RuleParser> ruleParserClass = rule.getRuleParserClass();
        System.out.println(ruleParserClass); //Prints "class RuleParserImpl"
    }
}

Hope that gives you some ideas.

Upvotes: 1

Related Questions