blob: 17db34fda1e4893095703748331fc4d3af391edb [file] [log] [blame]
package org.checkerframework.checker.calledmethods.builder;
import com.sun.source.tree.NewClassTree;
import com.sun.source.tree.VariableTree;
import java.beans.Introspector;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import org.checkerframework.checker.calledmethods.CalledMethodsAnnotatedTypeFactory;
import org.checkerframework.framework.type.AnnotatedTypeMirror;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType;
import org.checkerframework.javacutil.AnnotationUtils;
import org.checkerframework.javacutil.ElementUtils;
/**
* Lombok support for the Called Methods Checker. This class adds CalledMethods annotations to the
* code generated by Lombok.
*/
public class LombokSupport implements BuilderFrameworkSupport {
/** The type factory. */
private CalledMethodsAnnotatedTypeFactory atypeFactory;
/**
* Create a new LombokSupport.
*
* @param atypeFactory the typechecker's type factory
*/
public LombokSupport(CalledMethodsAnnotatedTypeFactory atypeFactory) {
this.atypeFactory = atypeFactory;
}
// The list is copied from lombok.core.handlers.HandlerUtil. The list cannot be used from that
// class directly because Lombok does not provide class files for its own implementation, to
// prevent itself from being accidentally added to clients' compile classpaths. This design
// decision means that it is impossible to depend directly on Lombok internals.
/** The list of annotations that Lombok treats as non-null. */
public static final List<String> NONNULL_ANNOTATIONS =
Collections.unmodifiableList(
Arrays.asList(
"android.annotation.NonNull",
"android.support.annotation.NonNull",
"com.sun.istack.internal.NotNull",
"edu.umd.cs.findbugs.annotations.NonNull",
"javax.annotation.Nonnull",
// "javax.validation.constraints.NotNull", // The field might contain a
// null value until it is persisted.
"lombok.NonNull",
"org.checkerframework.checker.nullness.qual.NonNull",
"org.eclipse.jdt.annotation.NonNull",
"org.eclipse.jgit.annotations.NonNull",
"org.jetbrains.annotations.NotNull",
"org.jmlspecs.annotation.NonNull",
"org.netbeans.api.annotations.common.NonNull",
"org.springframework.lang.NonNull"));
/**
* A map from elements that have a lombok.Builder.Default annotation to the simple property name
* that should be treated as defaulted.
*
* <p>This cache is kept because the usual method for checking that an element has been defaulted
* (calling declarationFromElement and examining the resulting VariableTree) only works if a
* corresponding Tree is available (for code that is only available as bytecode, no such Tree is
* available and that method returns null). See the code in {@link
* #getLombokRequiredProperties(Element)} that handles fields.
*/
private final Map<Element, Name> defaultedElements = new HashMap<>(2);
@Override
public boolean isBuilderBuildMethod(ExecutableElement candidateBuildElement) {
TypeElement candidateGeneratedBuilderElement =
(TypeElement) candidateBuildElement.getEnclosingElement();
if ((ElementUtils.hasAnnotation(candidateGeneratedBuilderElement, "lombok.Generated")
|| ElementUtils.hasAnnotation(candidateBuildElement, "lombok.Generated"))
&& candidateGeneratedBuilderElement.getSimpleName().toString().endsWith("Builder")) {
return candidateBuildElement.getSimpleName().contentEquals("build");
}
return false;
}
@Override
public void handleBuilderBuildMethod(AnnotatedExecutableType builderBuildType) {
ExecutableElement buildElement = builderBuildType.getElement();
TypeElement generatedBuilderElement = (TypeElement) buildElement.getEnclosingElement();
// The class with the @lombok.Builder annotation...
Element annotatedWithBuilderElement = generatedBuilderElement.getEnclosingElement();
List<String> requiredProperties = getLombokRequiredProperties(annotatedWithBuilderElement);
AnnotationMirror newCalledMethodsAnno =
atypeFactory.createAccumulatorAnnotation(requiredProperties);
builderBuildType.getReceiverType().addAnnotation(newCalledMethodsAnno);
}
@Override
public boolean isToBuilderMethod(ExecutableElement candidateToBuilderElement) {
return candidateToBuilderElement.getSimpleName().contentEquals("toBuilder")
&& (ElementUtils.hasAnnotation(candidateToBuilderElement, "lombok.Generated")
|| ElementUtils.hasAnnotation(
candidateToBuilderElement.getEnclosingElement(), "lombok.Generated"));
}
@Override
public void handleToBuilderMethod(AnnotatedExecutableType toBuilderType) {
AnnotatedTypeMirror returnType = toBuilderType.getReturnType();
ExecutableElement buildElement = toBuilderType.getElement();
TypeElement generatedBuilderElement = (TypeElement) buildElement.getEnclosingElement();
handleToBuilderType(returnType, generatedBuilderElement);
}
/**
* Add, to a type, a CalledMethods annotation that states that all required setters have been
* called. The type can be the return type of toBuilder or of the corresponding generated "copy"
* constructor.
*
* @param type type to update
* @param classElement corresponding AutoValue class
*/
private void handleToBuilderType(AnnotatedTypeMirror type, Element classElement) {
List<String> requiredProperties = getLombokRequiredProperties(classElement);
AnnotationMirror calledMethodsAnno =
atypeFactory.createAccumulatorAnnotation(requiredProperties);
type.replaceAnnotation(calledMethodsAnno);
}
/**
* Computes the required properties of a @lombok.Builder class, i.e., the names of the fields
* with @lombok.NonNull annotations.
*
* @param lombokClassElement the class with the @lombok.Builder annotation
* @return a list of required property names
*/
private List<String> getLombokRequiredProperties(final Element lombokClassElement) {
List<String> requiredPropertyNames = new ArrayList<>();
List<String> defaultedPropertyNames = new ArrayList<>();
for (Element member : lombokClassElement.getEnclosedElements()) {
if (member.getKind() == ElementKind.FIELD) {
// Lombok never generates non-null fields with initializers in builders, unless the field is
// annotated with @Default or @Singular, which are handled elsewhere. So, this code doesn't
// need to consider whether the field has or does not have initializers.
for (AnnotationMirror anm :
atypeFactory.getElementUtils().getAllAnnotationMirrors(member)) {
if (NONNULL_ANNOTATIONS.contains(AnnotationUtils.annotationName(anm))) {
requiredPropertyNames.add(member.getSimpleName().toString());
}
}
} else if (member.getKind() == ElementKind.METHOD
&& ElementUtils.hasAnnotation(member, "lombok.Generated")) {
String methodName = member.getSimpleName().toString();
// If a field foo has an @Builder.Default annotation, Lombok always generates a
// method called $default$foo.
if (methodName.startsWith("$default$")) {
String propName = methodName.substring(9); // $default$ has 9 characters
defaultedPropertyNames.add(propName);
}
} else if (member.getKind().isClass() && member.toString().endsWith("Builder")) {
// Note that the test above will fail to catch builders generated by Lombok that have custom
// names using the builderClassName attribute. TODO: find a way to handle such builders too.
// If a field bar has an @Singular annotation, Lombok always generates a method called
// clearBar in the builder class itself. Therefore, search the builder for such a method,
// and extract the appropriate property name to treat as defaulted.
for (Element builderMember : member.getEnclosedElements()) {
if (builderMember.getKind() == ElementKind.METHOD
&& ElementUtils.hasAnnotation(builderMember, "lombok.Generated")) {
String methodName = builderMember.getSimpleName().toString();
if (methodName.startsWith("clear")) {
String propName =
Introspector.decapitalize(methodName.substring(5)); // clear has 5 characters
defaultedPropertyNames.add(propName);
}
} else if (builderMember.getKind() == ElementKind.FIELD) {
VariableTree variableTree =
(VariableTree) atypeFactory.declarationFromElement(builderMember);
if (variableTree != null && variableTree.getInitializer() != null) {
Name propName = variableTree.getName();
defaultedPropertyNames.add(propName.toString());
defaultedElements.put(builderMember, propName);
} else if (defaultedElements.containsKey(builderMember)) {
defaultedPropertyNames.add(defaultedElements.get(builderMember).toString());
}
}
}
}
}
requiredPropertyNames.removeAll(defaultedPropertyNames);
return requiredPropertyNames;
}
@Override
public void handleConstructor(NewClassTree tree, AnnotatedTypeMirror type) {
return;
}
}