package org.robolectric.annotation.processing.validator;
import com.sun.source.tree.ImportTree;
import com.sun.source.util.Trees;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.processing.DocumentedMethod;
import org.robolectric.annotation.processing.RobolectricModel;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;
import javax.tools.Diagnostic.Kind;
import java.util.ArrayList;
import java.util.List;
/**
* Validator that checks usages of {@link org.robolectric.annotation.Implements}.
*/
public class ImplementsValidator extends Validator {
public static final String IMPLEMENTS_CLASS = "org.robolectric.annotation.Implements";
public static final int MAX_SUPPORTED_ANDROID_SDK = 10000; // Now == Build.VERSION_CODES.O
private final ProcessingEnvironment env;
public ImplementsValidator(RobolectricModel model, ProcessingEnvironment env) {
super(model, env, IMPLEMENTS_CLASS);
this.env = env;
}
private TypeElement getClassNameTypeElement(AnnotationValue cv) {
String className = RobolectricModel.classNameVisitor.visit(cv);
TypeElement type = elements.getTypeElement(className.replace('$', '.'));
if (type == null) {
error("@Implements: could not resolve class <" + className + '>', cv);
return null;
}
return type;
}
@Override
public Void visitType(TypeElement elem, Element parent) {
captureJavadoc(elem);
// Don't import nested classes because some of them have the same name.
AnnotationMirror am = getCurrentAnnotation();
AnnotationValue av = RobolectricModel.getAnnotationValue(am, "value");
AnnotationValue cv = RobolectricModel.getAnnotationValue(am, "className");
AnnotationValue maxSdk = RobolectricModel.getAnnotationValue(am, "maxSdk");
// This shadow doesn't apply to the current SDK. todo: check each SDK.
if (maxSdk != null && RobolectricModel.intVisitor.visit(maxSdk) < MAX_SUPPORTED_ANDROID_SDK) {
String sdkClassName;
if (av == null) {
sdkClassName = RobolectricModel.classNameVisitor.visit(cv).replace('$', '.');
} else {
sdkClassName = av.toString();
}
// there's no such type at the current SDK level, so just use strings...
model.addExtraShadow(sdkClassName, elem.getQualifiedName().toString());
return null;
}
TypeElement type = null;
if (av == null) {
if (cv == null) {
error("@Implements: must specify <value> or <className>");
return null;
}
type = getClassNameTypeElement(cv);
} else {
TypeMirror value = RobolectricModel.valueVisitor.visit(av);
if (value == null) {
return null;
}
boolean isAnything = model.ANYTHING_MIRROR != null && types.isSameType(value, model.ANYTHING_MIRROR);
if (isAnything) {
if (cv == null) {
error("@Implements: Anything class specified but no <className> attribute");
return null;
}
type = getClassNameTypeElement(cv);
} else if (cv != null) {
error("@Implements: cannot specify both <value> and <className> attributes");
} else {
type = RobolectricModel.typeVisitor.visit(types.asElement(value));
}
}
if (type == null) {
return null;
}
final List<? extends TypeParameterElement> typeTP = type.getTypeParameters();
final List<? extends TypeParameterElement> elemTP = elem.getTypeParameters();
if (!model.isSameParameterList(typeTP, elemTP)) {
StringBuilder message = new StringBuilder();
if (elemTP.isEmpty()) {
message.append("Shadow type is missing type parameters, expected <");
model.appendParameterList(message, type.getTypeParameters());
message.append('>');
} else if (typeTP.isEmpty()) {
message.append("Shadow type has type parameters but real type does not");
} else {
message.append("Shadow type must have same type parameters as its real counterpart: expected <");
model.appendParameterList(message, type.getTypeParameters());
message.append(">, was <");
model.appendParameterList(message, elem.getTypeParameters());
message.append('>');
}
messager.printMessage(Kind.ERROR, message, elem);
return null;
}
model.addShadowType(elem, type);
return null;
}
private void captureJavadoc(TypeElement elem) {
List<String> imports = new ArrayList<>();
List<? extends ImportTree> importLines = Trees.instance(env).getPath(elem).getCompilationUnit().getImports();
for (ImportTree importLine : importLines) {
imports.add(importLine.getQualifiedIdentifier().toString());
}
List<TypeElement> enclosedTypes = ElementFilter.typesIn(elem.getEnclosedElements());
for (TypeElement enclosedType : enclosedTypes) {
imports.add(enclosedType.getQualifiedName().toString());
}
Elements elementUtils = env.getElementUtils();
model.documentType(elem, elementUtils.getDocComment(elem), imports);
for (Element memberElement : ElementFilter.methodsIn(elem.getEnclosedElements())) {
ExecutableElement methodElement = (ExecutableElement) memberElement;
Implementation implementation = memberElement.getAnnotation(Implementation.class);
DocumentedMethod documentedMethod = new DocumentedMethod(memberElement.toString());
for (Modifier modifier : memberElement.getModifiers()) {
documentedMethod.modifiers.add(modifier.toString());
}
documentedMethod.isImplementation = implementation != null;
if (implementation != null) {
documentedMethod.minSdk = sdkOrNull(implementation.minSdk());
documentedMethod.maxSdk = sdkOrNull(implementation.maxSdk());
}
for (VariableElement variableElement : methodElement.getParameters()) {
documentedMethod.params.add(variableElement.toString());
}
documentedMethod.returnType = methodElement.getReturnType().toString();
for (TypeMirror typeMirror : methodElement.getThrownTypes()) {
documentedMethod.exceptions.add(typeMirror.toString());
}
String docMd = elementUtils.getDocComment(methodElement);
if (docMd != null) {
documentedMethod.setDocumentation(docMd);
}
model.documentMethod(elem, documentedMethod);
}
}
private Integer sdkOrNull(int sdk) {
return sdk == -1 ? null : sdk;
}
}