// =================================================================================================
// Copyright 2011 Twitter, Inc.
// -------------------------------------------------------------------------------------------------
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this work except in compliance with the License.
// You may obtain a copy of the License in the LICENSE file, or at:
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =================================================================================================
package com.twitter.common.args.apt;
import java.io.Closeable;
import java.io.IOException;
import java.io.Writer;
import java.lang.annotation.Annotation;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedOptions;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.SimpleAnnotationValueVisitor6;
import javax.lang.model.util.SimpleTypeVisitor6;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic.Kind;
import javax.tools.FileObject;
import javax.tools.StandardLocation;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Predicates;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.twitter.common.args.Arg;
import com.twitter.common.args.ArgParser;
import com.twitter.common.args.CmdLine;
import com.twitter.common.args.Parser;
import com.twitter.common.args.Positional;
import com.twitter.common.args.Verifier;
import com.twitter.common.args.VerifierFor;
import com.twitter.common.args.apt.Configuration.ParserInfo;
import static com.twitter.common.args.apt.Configuration.ArgInfo;
/**
* Processes {@literal @CmdLine} annotated fields and {@literal @ArgParser} and
* {@literal @VerifierFor} parser and verifier registrations and stores configuration data listing
* these fields, parsers and verifiers on the classpath for discovery via
* {@link com.twitter.common.args.apt.Configuration#load()}.
*/
@SupportedOptions({
CmdLineProcessor.MAIN_OPTION,
CmdLineProcessor.CHECK_LINKAGE_OPTION
})
public class CmdLineProcessor extends AbstractProcessor {
static final String MAIN_OPTION =
"com.twitter.common.args.apt.CmdLineProcessor.main";
static final String CHECK_LINKAGE_OPTION =
"com.twitter.common.args.apt.CmdLineProcessor.check_linkage";
private static final Function<Class<?>, String> GET_NAME = new Function<Class<?>, String>() {
@Override public String apply(Class<?> type) {
return type.getName();
}
};
/** Existing merged Configuration loaded from the classpath. */
private Supplier<Configuration> persistedConfig =
Suppliers.memoize(new Supplier<Configuration>() {
@Override public Configuration get() {
try {
return Configuration.load();
} catch (IOException e) {
throw new RuntimeException("Failed to load existing Arg fields:", e);
}
}
});
/** New configurations for this round. */
private Map<String, Configuration.Builder> roundConfigs =
new HashMap<String, Configuration.Builder>();
/** Relevant classnames that were alive in any round. */
private final ImmutableSet.Builder<String> liveClassNamesBuilder = ImmutableSet.builder();
private Types typeUtils;
private Elements elementUtils;
private boolean isMain;
private boolean isCheckLinkage;
private static boolean getBooleanOption(Map<String, String> options, String name,
boolean defaultValue) {
if (!options.containsKey(name)) {
return defaultValue;
}
// We want to map the presence of a boolean option without a value to indicate true, giving the
// following accepted boolean option formats:
// -Afoo -> true
// -Afoo=false -> false
// -Afoo=true -> true
String isOption = options.get(name);
return (isOption == null) || Boolean.parseBoolean(isOption);
}
@Override
public void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
typeUtils = processingEnv.getTypeUtils();
elementUtils = processingEnv.getElementUtils();
Map<String, String> options = processingEnv.getOptions();
isMain = getBooleanOption(options, MAIN_OPTION, false);
isCheckLinkage = getBooleanOption(options, CHECK_LINKAGE_OPTION, true);
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latest();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
return ImmutableSet.copyOf(Iterables.transform(
ImmutableList.of(Positional.class, CmdLine.class, ArgParser.class, VerifierFor.class),
GET_NAME));
}
/** Get or create a Configuration.Builder for the current round. */
private Configuration.Builder getBuilder(String className) {
Configuration.Builder builder = this.roundConfigs.get(className);
if (builder == null) {
builder = new Configuration.Builder(className);
this.roundConfigs.put(className, builder);
}
return builder;
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
try {
// Collect all live classnames from this round, so that if they lose all annotated fields, we
// can delete their previous resources.
liveClassNamesBuilder.addAll(extractClassNames(roundEnv));
// Then collect all relevant annotated classes.
Set<? extends Element> parsers = getAnnotatedElements(roundEnv, ArgParser.class);
@Nullable Set<String> parsedTypes = getParsedTypes(parsers);
Set<? extends Element> cmdlineArgs = getAnnotatedElements(roundEnv, CmdLine.class);
Set<? extends Element> positionalArgs = getAnnotatedElements(roundEnv, Positional.class);
ImmutableSet<? extends Element> invalidArgs =
Sets.intersection(cmdlineArgs, positionalArgs).immutableCopy();
if (!invalidArgs.isEmpty()) {
error("An Arg cannot be annotated with both @CmdLine and @Positional, found bad Arg "
+ "fields: %s", invalidArgs);
}
for (ArgInfo cmdLineInfo : processAnnotatedArgs(parsedTypes, cmdlineArgs, CmdLine.class)) {
getBuilder(cmdLineInfo.className).addCmdLineArg(cmdLineInfo);
}
for (ArgInfo positionalInfo
: processAnnotatedArgs(parsedTypes, positionalArgs, Positional.class)) {
getBuilder(positionalInfo.className).addPositionalInfo(positionalInfo);
}
checkPositionalArgsAreLists(roundEnv);
processParsers(parsers);
Set<? extends Element> verifiers = getAnnotatedElements(roundEnv, VerifierFor.class);
processVerifiers(verifiers);
if (roundEnv.processingOver()) {
for (String className : this.liveClassNamesBuilder.build()) {
FileObject cmdLinePropertiesResource = createCommandLineDb(className);
Configuration.Builder configBuilder = getBuilder(className);
// Delete the config resource for classes which no longer exist, or which have
// no fields.
if (configBuilder.isEmpty()) {
cmdLinePropertiesResource.delete();
continue;
}
// Otherwise, write a new copy of the resource.
Writer writer = null;
try {
writer = cmdLinePropertiesResource.openWriter();
configBuilder.build().store(writer, "Generated via apt by " + getClass().getName());
} catch (IOException e) {
throw new RuntimeException(
"Failed to write Arg resource file for " + className + ":",
e);
} finally {
closeQuietly(writer);
}
}
}
} catch (RuntimeException e) { // SUPPRESS CHECKSTYLE IllegalCatch
// Catch internal errors - when these bubble more useful queued error messages are lost in
// some javac implementations.
error("Unexpected error completing annotation processing:\n%s",
Throwables.getStackTraceAsString(e));
}
return true;
}
/** Return classNames from the given RoundEnvironment. */
private Iterable<String> extractClassNames(RoundEnvironment roundEnv) {
ImmutableList.Builder<String> classNames = new ImmutableList.Builder<String>();
for (Element element : roundEnv.getRootElements()) {
classNames.addAll(extractClassNames(element));
}
return classNames.build();
}
/** If the given element is a class, returns its className, and those of its inner classes. */
private Iterable<String> extractClassNames(Element element) {
if (!(element instanceof TypeElement)) {
return ImmutableList.<String>of();
}
Iterable<String> classNames = ImmutableList.of(getBinaryName((TypeElement) element));
for (Element child : element.getEnclosedElements()) {
classNames = Iterables.concat(classNames, extractClassNames(child));
}
return classNames;
}
private void closeQuietly(Closeable closeable) {
if (closeable == null) {
return;
}
try {
closeable.close();
} catch (IOException e) {
log(Kind.MANDATORY_WARNING, "Failed to close %s: %s", closeable, e);
}
}
private void checkPositionalArgsAreLists(RoundEnvironment roundEnv) {
for (Element positionalArg : getAnnotatedElements(roundEnv, Positional.class)) {
@Nullable TypeMirror typeArgument =
getTypeArgument(positionalArg.asType(), typeElement(Arg.class));
if ((typeArgument == null)
|| !typeUtils.isSubtype(typeElement(List.class).asType(), typeArgument)) {
error("Found @Positional %s %s.%s that is not a List",
positionalArg.asType(), positionalArg.getEnclosingElement(), positionalArg);
}
}
}
@Nullable
private Set<String> getParsedTypes(Set<? extends Element> parsers) {
if (!isCheckLinkage) {
return null;
}
Iterable<String> parsersFor = Optional.presentInstances(Iterables.transform(parsers,
new Function<Element, Optional<String>>() {
@Override public Optional<String> apply(Element parser) {
TypeMirror parsedType = getTypeArgument(parser.asType(), typeElement(Parser.class));
if (parsedType == null) {
error("failed to find a type argument for Parser: %s", parser);
return Optional.absent();
}
// Equals on TypeMirrors doesn't work - so we compare string representations :/
return Optional.of(typeUtils.erasure(parsedType).toString());
}
}));
parsersFor = Iterables.concat(parsersFor, Iterables.filter(
Iterables.transform(this.persistedConfig.get().parserInfo(),
new Function<ParserInfo, String>() {
@Override @Nullable public String apply(ParserInfo parserInfo) {
TypeElement typeElement = elementUtils.getTypeElement(parserInfo.parsedType);
// We may not have a type on the classpath for a previous round - this is fine as
// long as the no Args in this round that are of the type.
return (typeElement == null)
? null : typeUtils.erasure(typeElement.asType()).toString();
}
}), Predicates.notNull()));
return ImmutableSet.copyOf(parsersFor);
}
private Iterable<ArgInfo> processAnnotatedArgs(
@Nullable final Set<String> parsedTypes,
Set<? extends Element> args,
final Class<? extends Annotation> argAnnotation) {
return Optional.presentInstances(Iterables.transform(args,
new Function<Element, Optional<ArgInfo>>() {
@Override public Optional<ArgInfo> apply(Element arg) {
@Nullable TypeElement containingType = processArg(parsedTypes, arg, argAnnotation);
if (containingType == null) {
return Optional.absent();
} else {
return Optional.of(new ArgInfo(getBinaryName(containingType),
arg.getSimpleName().toString()));
}
}
}));
}
private Set<? extends Element> getAnnotatedElements(RoundEnvironment roundEnv,
Class<? extends Annotation> argAnnotation) {
return roundEnv.getElementsAnnotatedWith(typeElement(argAnnotation));
}
@Nullable
private TypeElement processArg(@Nullable Set<String> parsedTypes, Element annotationElement,
Class<? extends Annotation> annotationType) {
TypeElement parserType = typeElement(Parser.class);
if (annotationElement.getKind() != ElementKind.FIELD) {
error("Found a @%s annotation on a non-field %s",
annotationType.getSimpleName(), annotationElement);
return null;
} else {
// Only types contain fields so this cast is safe.
TypeElement containingType = (TypeElement) annotationElement.getEnclosingElement();
if (!isAssignable(annotationElement.asType(), Arg.class)) {
error("Found a @%s annotation on a non-Arg %s.%s",
annotationType.getSimpleName(), containingType, annotationElement);
return null;
}
if (!annotationElement.getModifiers().contains(Modifier.STATIC)) {
return null;
}
if (parsedTypes != null) {
// Check Parser<T> linkage for the Arg<T> type T.
TypeMirror typeArgument =
getTypeArgument(annotationElement.asType(), typeElement(Arg.class));
@Nullable AnnotationMirror cmdLine =
getAnnotationMirror(annotationElement, typeElement(annotationType));
if (cmdLine != null) {
TypeMirror customParserType = getClassType(cmdLine, "parser", parserType).asType();
if (typeUtils.isSameType(parserType.asType(), customParserType)) {
if (!checkTypePresent(parsedTypes, typeArgument)) {
error("No parser registered for %s; %s.%s is un-parseable",
typeArgument, containingType, annotationElement);
}
} else {
TypeMirror customParsedType = getTypeArgument(customParserType, parserType);
if (!isAssignable(typeArgument, customParsedType)) {
error("Custom parser %s parses %s but registered for %s.%s with Arg type %s",
customParserType, customParsedType, containingType, annotationElement,
typeArgument);
}
}
}
}
// TODO(John Sirois): Add additional compile-time @CmdLine verification for:
// 1.) for each @CmdLine Arg<T> annotated with @VerifierFor.annotation: T is a subtype of
// V where there is a Verifier<V>
// 2.) name checks, including dups
return containingType;
}
}
private boolean checkTypePresent(Set<String> types, TypeMirror type) {
Iterable<TypeMirror> allTypes = getAllTypes(type);
for (TypeMirror t : allTypes) {
if (types.contains(typeUtils.erasure(t).toString())) {
return true;
}
}
return false;
}
private void processParsers(Set<? extends Element> elements) {
TypeElement parserType = typeElement(Parser.class);
for (Element element : elements) {
if (element.getKind() != ElementKind.CLASS) {
error("Found an @ArgParser annotation on a non-class %s", element);
} else {
TypeElement parser = (TypeElement) element;
if (!isAssignable(parser, Parser.class)) {
error("Found an @ArgParser annotation on a non-Parser %s", element);
return;
}
@Nullable String parsedType = getTypeArgument(parser, parserType);
if (parsedType != null) {
String parserClassName = getBinaryName(parser);
getBuilder(parserClassName).addParser(parsedType, getBinaryName(parser));
}
}
}
}
private void processVerifiers(Set<? extends Element> elements) {
TypeElement verifierType = typeElement(Verifier.class);
TypeElement verifierForType = typeElement(VerifierFor.class);
for (Element element : elements) {
if (element.getKind() != ElementKind.CLASS) {
error("Found a @VerifierFor annotation on a non-class %s", element);
} else {
TypeElement verifier = (TypeElement) element;
if (!isAssignable(verifier, Verifier.class)) {
error("Found a @Verifier annotation on a non-Verifier %s", element);
return;
}
String verifierClassName = getBinaryName(verifier);
@Nullable AnnotationMirror verifierFor = getAnnotationMirror(verifier, verifierForType);
if (verifierFor != null) {
@Nullable TypeElement verifyAnnotationType = getClassType(verifierFor, "value", null);
if (verifyAnnotationType != null) {
@Nullable String verifiedType = getTypeArgument(verifier, verifierType);
if (verifiedType != null) {
String verifyAnnotationClassName =
elementUtils.getBinaryName(verifyAnnotationType).toString();
getBuilder(verifierClassName).addVerifier(verifiedType, verifyAnnotationClassName,
verifierClassName);
}
}
}
}
}
}
@Nullable
private String getTypeArgument(TypeElement annotatedType, final TypeElement baseType) {
TypeMirror typeArgument = getTypeArgument(annotatedType.asType(), baseType);
return typeArgument == null
? null
: getBinaryName((TypeElement) typeUtils.asElement(typeArgument));
}
private Iterable<TypeMirror> getAllTypes(TypeMirror type) {
return getAllTypes(new HashSet<String>(), Lists.<TypeMirror>newArrayList(), type);
}
private Iterable<TypeMirror> getAllTypes(Set<String> visitedTypes, List<TypeMirror> types,
TypeMirror type) {
String typeName = typeUtils.erasure(type).toString();
if (!visitedTypes.contains(typeName)) {
types.add(type);
visitedTypes.add(typeName);
for (TypeMirror superType : typeUtils.directSupertypes(type)) {
getAllTypes(visitedTypes, types, superType);
}
}
return types;
}
@Nullable
private TypeMirror getTypeArgument(TypeMirror annotatedType, final TypeElement baseType) {
for (TypeMirror type : getAllTypes(annotatedType)) {
TypeMirror typeArgument = type.accept(new SimpleTypeVisitor6<TypeMirror, Void>() {
@Override public TypeMirror visitDeclared(DeclaredType t, Void aVoid) {
if (isAssignable(t, baseType)) {
List<? extends TypeMirror> typeArguments = t.getTypeArguments();
if (!typeArguments.isEmpty()) {
return typeUtils.erasure(typeArguments.get(0));
}
}
return null;
}
}, null);
if (typeArgument != null) {
return typeArgument;
}
}
error("Failed to find a type argument for %s in %s", baseType, annotatedType);
return null;
}
@Nullable
private AnnotationMirror getAnnotationMirror(Element element, TypeElement annotationType) {
for (AnnotationMirror annotationMirror : element.getAnnotationMirrors()) {
if (typeUtils.isSameType(annotationMirror.getAnnotationType(), annotationType.asType())) {
return annotationMirror;
}
}
error("Failed to find an annotation of type %s on %s", annotationType, element);
return null;
}
@SuppressWarnings("unchecked")
private TypeElement getClassType(AnnotationMirror annotationMirror, String methodName,
TypeElement defaultClassType) {
for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry
: annotationMirror.getElementValues().entrySet()) {
if (entry.getKey().getSimpleName().equals(elementUtils.getName(methodName))) {
TypeElement classType = entry.getValue().accept(
new SimpleAnnotationValueVisitor6<TypeElement, Void>() {
@Override public TypeElement visitType(TypeMirror t, Void unused) {
return (TypeElement) processingEnv.getTypeUtils().asElement(t);
}
}, null);
if (classType != null) {
return classType;
}
}
}
if (defaultClassType == null) {
error("Could not find a class type for %s.%s", annotationMirror, methodName);
}
return defaultClassType;
}
@Nullable
private FileObject createCommandLineDb(String className) {
// TODO: Move into the Configuration package somehow.
return createResource(
Configuration.DEFAULT_RESOURCE_PACKAGE,
className + Configuration.DEFAULT_RESOURCE_SUFFIX);
}
@Nullable
private FileObject createResource(String packageName, String name) {
try {
return processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT,
packageName, name);
} catch (IOException e) {
throw new RuntimeException(
"Failed to create resource file to store %s/%s:".format(packageName, name), e);
}
}
private TypeElement typeElement(Class<?> type) {
return elementUtils.getTypeElement(type.getName());
}
private String getBinaryName(TypeElement typeElement) {
return elementUtils.getBinaryName(typeElement).toString();
}
private boolean isAssignable(TypeElement subType, Class<?> baseType) {
return isAssignable(subType.asType(), baseType);
}
private boolean isAssignable(TypeMirror subType, Class<?> baseType) {
return isAssignable(subType, typeElement(baseType));
}
private boolean isAssignable(TypeMirror subType, TypeElement baseType) {
return isAssignable(subType, baseType.asType());
}
private boolean isAssignable(TypeMirror subType, TypeMirror baseType) {
return typeUtils.isAssignable(typeUtils.erasure(subType), typeUtils.erasure(baseType));
}
private void error(String message, Object ... args) {
log(Kind.ERROR, message, args);
}
private void log(Kind kind, String message, Object ... args) {
processingEnv.getMessager().printMessage(kind, String.format(message, args));
}
}