Skip to content

Commit

Permalink
[#500] Extract validation, CompileTimeTypeInfo and TypedMember from t…
Browse files Browse the repository at this point in the history
…he annotation processor class
  • Loading branch information
remkop committed May 19, 2019
1 parent 851aeac commit 7cfca56
Show file tree
Hide file tree
Showing 3 changed files with 437 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package picocli.codegen.annotation.processing;

import picocli.CommandLine;
import picocli.codegen.util.Assert;

import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.tools.Diagnostic;
import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;

import static java.lang.String.format;

public class AnnotationValidator {

private static final Set<Class<? extends Annotation>> ALL = Collections.unmodifiableSet(
new LinkedHashSet<Class<? extends Annotation>>(Arrays.asList(
CommandLine.Command.class,
CommandLine.Option.class,
CommandLine.Parameters.class,
CommandLine.Mixin.class,
CommandLine.ParentCommand.class,
CommandLine.Spec.class,
CommandLine.Unmatched.class,
CommandLine.ArgGroup.class
))
);
private ProcessingEnvironment processingEnv;

public AnnotationValidator(ProcessingEnvironment processingEnv) {
this.processingEnv = Assert.notNull(processingEnv, "processingEnv");
}

public void validateAnnotations(RoundEnvironment roundEnv) {
validateNoAnnotationsOnInterfaceField(roundEnv);
validateInvalidCombination(roundEnv, CommandLine.Mixin.class, CommandLine.Option.class);
validateInvalidCombination(roundEnv, CommandLine.Mixin.class, CommandLine.Parameters.class);
validateInvalidCombination(roundEnv, CommandLine.Mixin.class, CommandLine.Unmatched.class);
validateInvalidCombination(roundEnv, CommandLine.Mixin.class, CommandLine.Spec.class);
validateInvalidCombination(roundEnv, CommandLine.Unmatched.class, CommandLine.Option.class);
validateInvalidCombination(roundEnv, CommandLine.Unmatched.class, CommandLine.Parameters.class);
validateInvalidCombination(roundEnv, CommandLine.Spec.class, CommandLine.Option.class);
validateInvalidCombination(roundEnv, CommandLine.Spec.class, CommandLine.Parameters.class);
validateInvalidCombination(roundEnv, CommandLine.Spec.class, CommandLine.Unmatched.class);
validateInvalidCombination(roundEnv, CommandLine.Option.class, CommandLine.Parameters.class);

// TODO
//validateSpecFieldTypeIsCommandSpec(roundEnv);
//validateOptionOrParametersIsNotFinalPrimitiveOrFinalString(roundEnv);
//validateUnmatchedFieldTypeIsStringArrayOrListOfString(roundEnv);
}

private void validateNoAnnotationsOnInterfaceField(RoundEnvironment roundEnv) {
for (Class<? extends Annotation> cls : ALL) {
validateNoAnnotationsOnInterfaceField(roundEnv.getElementsAnnotatedWith(cls));
}
}

private void validateNoAnnotationsOnInterfaceField(Set<? extends Element> all) {
for (Element element : all) {
if (element.getKind() == ElementKind.FIELD &&
element.getEnclosingElement().getKind() == ElementKind.INTERFACE) {
error(element, "Invalid picocli annotation on interface field %s.%s",
element.getEnclosingElement().toString(), element.getSimpleName());
}
}
}

private <T1 extends Annotation, T2 extends Annotation> void validateInvalidCombination(
RoundEnvironment roundEnv, Class<T1> c1, Class<T2> c2) {
for (Element element : roundEnv.getElementsAnnotatedWith(c1)) {
if (element.getAnnotation(c2) != null) {
error(element, "%s cannot have both @%s and @%s annotations",
element, c1.getCanonicalName(), c2.getCanonicalName());
}
}
}

void error(Element e, String msg, Object... args) {
processingEnv.getMessager().printMessage(
Diagnostic.Kind.ERROR,
format(msg, args),
e);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
package picocli.codegen.annotation.processing;

import picocli.CommandLine;

import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Logger;

class CompileTimeTypeInfo implements CommandLine.Model.ITypeInfo {
private static Logger logger = Logger.getLogger(CompileTimeTypeInfo.class.getName());
private static final EnumSet<TypeKind> PRIMITIVES = EnumSet.of(
TypeKind.BYTE, TypeKind.BOOLEAN, TypeKind.CHAR, TypeKind.DOUBLE,
TypeKind.FLOAT, TypeKind.INT, TypeKind.LONG, TypeKind.SHORT);

final TypeMirror typeMirror;
final List<? extends TypeMirror> auxTypeMirrors;
final List<String> actualGenericTypeArguments;
final TypeElement typeElement;
final boolean isCollection;
final boolean isMap;

public CompileTimeTypeInfo(TypeMirror asType) {
typeMirror = asType;

// for non-multi-value types, the auxiliary type is a single-value list with the type
List<? extends TypeMirror> aux = Arrays.asList(typeMirror);
TypeElement tempTypeElement = null;
boolean collection = false;
boolean map = false;

if (typeMirror.getKind() == TypeKind.DECLARED) {
logger.finest("CompileTimeTypeInfo DECLARED typeMirror " + typeMirror);
Element element = ((DeclaredType) typeMirror).asElement();
if (element.getKind().isClass() || element.getKind().isInterface()) {
tempTypeElement = (TypeElement) element;
logger.finest("element is class or interface " + tempTypeElement);
map = find("java.util.Map", tempTypeElement);
collection = !map && find("java.util.Collection", tempTypeElement);
}
aux = ((DeclaredType) typeMirror).getTypeArguments();
actualGenericTypeArguments = new ArrayList<String>();
for (TypeMirror typeMirror : aux) {
actualGenericTypeArguments.add(typeMirror.toString());
}
logger.finest("aux (type args): " + aux);
if (aux.isEmpty()) {
if (map || collection) {
aux = Arrays.asList(createStringTypeMirror(), createStringTypeMirror());
logger.finest("fixed aux (for multi type): " + aux);
} else {
aux = Arrays.asList(typeMirror);
logger.finest("fixed aux (for single type): " + aux);
}
}
} else if (typeMirror.getKind() == TypeKind.ARRAY) {
aux = Arrays.asList(((ArrayType) typeMirror).getComponentType());
actualGenericTypeArguments = Arrays.asList(aux.get(0).toString());
} else {
actualGenericTypeArguments = Collections.emptyList();
}
auxTypeMirrors = aux;
typeElement = tempTypeElement;
isCollection = collection;
isMap = map;
}

private TypeMirror createStringTypeMirror() {
TypeElement element = typeElement;
while (element.getSuperclass().getKind() != TypeKind.NONE) {
logger.finest("finding toString in " + element);

element = (TypeElement) ((DeclaredType) element.getSuperclass()).asElement();
}
for (Element enclosed : typeElement.getEnclosedElements()) {
if (enclosed.getKind() == ElementKind.METHOD) {
ExecutableElement method = (ExecutableElement) enclosed;
if (method.getSimpleName().contentEquals("toString")) {
return method.getReturnType();
}
}
}
throw new IllegalStateException("Cannot find toString method in Object");
}
private static boolean find(String interfaceName, TypeElement typeElement) {
return find(interfaceName, typeElement, new HashSet<Element>());
}
private static boolean find(String interfaceName, TypeElement typeElement, Set<Element> visited) {
if (visited.contains(typeElement)) { return false; }
visited.add(typeElement);
//logger.finest("trying to find " + interfaceName + " in " + typeElement);

if (typeElement.getQualifiedName().contentEquals(interfaceName)) {
return true;
}
for (TypeMirror implemented : typeElement.getInterfaces()) {
if (find(interfaceName, (TypeElement) ((DeclaredType) implemented).asElement())) {
return true;
}
}
while (typeElement.getSuperclass().getKind() != TypeKind.NONE) {
typeElement = (TypeElement) ((DeclaredType) typeElement.getSuperclass()).asElement();
if (find(interfaceName, typeElement)) {
return true;
}
}
return false;
}

@Override
public List<CommandLine.Model.ITypeInfo> getAuxiliaryTypeInfos() {
// for non-multi-value types, the auxiliary type is a single-value list with the type
if (!isMultiValue()) {
logger.fine("getAuxiliaryTypeInfos (non-multi) returning new list with this");
return Arrays.<CommandLine.Model.ITypeInfo>asList(this);
}

List<CommandLine.Model.ITypeInfo> result = new ArrayList<CommandLine.Model.ITypeInfo>();
for (TypeMirror typeMirror : auxTypeMirrors) {
result.add(new CompileTimeTypeInfo(typeMirror));
}
logger.fine("getAuxiliaryTypeInfos (multi) returning list " + result);
return result;
}

@Override
public List<String> getActualGenericTypeArguments() {
return actualGenericTypeArguments;
}

@Override
public boolean isBoolean() {
TypeMirror type = auxTypeMirrors.get(0);
return type.getKind() == TypeKind.BOOLEAN || "java.lang.Boolean".equals(type.toString());
}

@Override
public boolean isMultiValue() {
return isArray() || isCollection() || isMap();
}

@Override
public boolean isArray() {
return typeMirror.getKind() == TypeKind.ARRAY;
}

@Override
public boolean isCollection() {
return isCollection;
}

@Override
public boolean isMap() {
return isMap;
}

@Override
public boolean isEnum() {
TypeMirror type = auxTypeMirrors.get(0);
return type.getKind() == TypeKind.DECLARED &&
((DeclaredType) type).asElement().getKind() == ElementKind.ENUM;
}

@Override
public List<String> getEnumConstantNames() {
if (!isEnum()) {
return Collections.emptyList();
}
List<String> result = new ArrayList<String>();
TypeMirror type = auxTypeMirrors.get(0);
List<? extends Element> enclosed = ((DeclaredType) type).asElement().getEnclosedElements();
for (Element element : enclosed) {
if (element.getKind() == ElementKind.ENUM_CONSTANT) {
result.add(element.toString());
}
}
return result;
}

@Override
public String getClassName() {
return typeElement == null ? typeMirror.toString() : typeElement.getQualifiedName().toString();
}

@Override
public String getClassSimpleName() {
return typeElement == null ? typeMirror.toString() : typeElement.getSimpleName().toString();
}

@Override
public Class<?> getType() {
return null;
}

@Override
public Class<?>[] getAuxiliaryTypes() {
return new Class[0];
}

@Override
public String toString() {
return String.format("CompileTimeTypeInfo(%s, aux=%s, collection=%s, map=%s)",
typeMirror, Arrays.toString(auxTypeMirrors.toArray()), isCollection, isMap);
}
}
Loading

0 comments on commit 7cfca56

Please sign in to comment.