blob: b6a578fcf0a60ba415092f43dbcee65b7ef7719d [file] [log] [blame]
/*
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License 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.google.auto.value.processor;
import static com.google.auto.common.MoreElements.getLocalAndInheritedMethods;
import static com.google.auto.common.MoreElements.getPackage;
import static com.google.auto.common.MoreStreams.toImmutableList;
import static com.google.auto.common.MoreStreams.toImmutableSet;
import static com.google.auto.value.processor.AutoValueProcessor.OMIT_IDENTIFIERS_OPTION;
import static com.google.auto.value.processor.ClassNames.AUTO_BUILDER_NAME;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toCollection;
import static java.util.stream.Collectors.toMap;
import static javax.lang.model.util.ElementFilter.constructorsIn;
import static javax.lang.model.util.ElementFilter.methodsIn;
import com.google.auto.common.AnnotationMirrors;
import com.google.auto.common.AnnotationValues;
import com.google.auto.common.MoreElements;
import com.google.auto.common.MoreTypes;
import com.google.auto.common.Visibility;
import com.google.auto.service.AutoService;
import com.google.auto.value.processor.BuilderSpec.PropertyGetter;
import com.google.auto.value.processor.MissingTypes.MissingTypeException;
import com.google.common.base.Ascii;
import com.google.common.base.VerifyException;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Map;
import java.util.NavigableSet;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Stream;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.SupportedAnnotationTypes;
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.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
import net.ltgt.gradle.incap.IncrementalAnnotationProcessor;
import net.ltgt.gradle.incap.IncrementalAnnotationProcessorType;
/**
* Javac annotation processor (compiler plugin) for builders; user code never references this class.
*
* @see <a href="https://github.com/google/auto/tree/master/value">AutoValue User's Guide</a>
* @author Éamonn McManus
*/
@AutoService(Processor.class)
@SupportedAnnotationTypes(AUTO_BUILDER_NAME)
@IncrementalAnnotationProcessor(IncrementalAnnotationProcessorType.ISOLATING)
public class AutoBuilderProcessor extends AutoValueishProcessor {
private static final String ALLOW_OPTION = "com.google.auto.value.AutoBuilderIsUnstable";
public AutoBuilderProcessor() {
super(AUTO_BUILDER_NAME);
}
@Override
public Set<String> getSupportedOptions() {
return ImmutableSet.of(OMIT_IDENTIFIERS_OPTION, ALLOW_OPTION);
}
private TypeMirror javaLangVoid;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
javaLangVoid = elementUtils().getTypeElement("java.lang.Void").asType();
}
@Override
void processType(TypeElement autoBuilderType) {
if (!processingEnv.getOptions().containsKey(ALLOW_OPTION)) {
errorReporter()
.abortWithError(
autoBuilderType,
"Compile with -A%s to enable this UNSUPPORTED AND UNSTABLE prototype",
ALLOW_OPTION);
}
if (autoBuilderType.getKind() != ElementKind.CLASS
&& autoBuilderType.getKind() != ElementKind.INTERFACE) {
errorReporter()
.abortWithError(
autoBuilderType,
"[AutoBuilderWrongType] @AutoBuilder only applies to classes and interfaces");
}
checkModifiersIfNested(autoBuilderType);
// The annotation is guaranteed to be present by the contract of Processor#process
AnnotationMirror autoBuilderAnnotation =
getAnnotationMirror(autoBuilderType, AUTO_BUILDER_NAME).get();
TypeElement ofClass = getOfClass(autoBuilderType, autoBuilderAnnotation);
checkModifiersIfNested(ofClass, autoBuilderType, "AutoBuilder ofClass");
String callMethod = findCallMethodValue(autoBuilderAnnotation);
ImmutableSet<ExecutableElement> methods =
abstractMethodsIn(
getLocalAndInheritedMethods(autoBuilderType, typeUtils(), elementUtils()));
ExecutableElement executable = findExecutable(ofClass, callMethod, autoBuilderType, methods);
BuilderSpec builderSpec = new BuilderSpec(ofClass, processingEnv, errorReporter());
BuilderSpec.Builder builder = builderSpec.new Builder(autoBuilderType);
TypeMirror builtType = builtType(executable);
Optional<BuilderMethodClassifier<VariableElement>> maybeClassifier =
BuilderMethodClassifierForAutoBuilder.classify(
methods, errorReporter(), processingEnv, executable, builtType, autoBuilderType);
if (!maybeClassifier.isPresent()) {
// We've already output one or more error messages.
return;
}
BuilderMethodClassifier<VariableElement> classifier = maybeClassifier.get();
Map<String, String> propertyToGetterName =
Maps.transformValues(classifier.builderGetters(), PropertyGetter::getName);
AutoBuilderTemplateVars vars = new AutoBuilderTemplateVars();
vars.props = propertySet(executable, propertyToGetterName);
builder.defineVars(vars, classifier);
vars.identifiers = !processingEnv.getOptions().containsKey(OMIT_IDENTIFIERS_OPTION);
String generatedClassName = generatedClassName(autoBuilderType, "AutoBuilder_");
vars.builderName = TypeSimplifier.simpleNameOf(generatedClassName);
vars.builtType = TypeEncoder.encode(builtType);
vars.build = build(executable);
vars.types = typeUtils();
vars.toBuilderConstructor = false;
vars.toBuilderMethods = ImmutableList.of();
defineSharedVarsForType(autoBuilderType, ImmutableSet.of(), vars);
String text = vars.toText();
text = TypeEncoder.decode(text, processingEnv, vars.pkg, autoBuilderType.asType());
text = Reformatter.fixup(text);
writeSourceFile(generatedClassName, text, autoBuilderType);
}
private ImmutableSet<Property> propertySet(
ExecutableElement executable, Map<String, String> propertyToGetterName) {
// Fix any parameter names that are reserved words in Java. Java source code can't have
// such parameter names, but Kotlin code might, for example.
Map<VariableElement, String> identifiers =
executable.getParameters().stream()
.collect(toMap(v -> v, v -> v.getSimpleName().toString()));
fixReservedIdentifiers(identifiers);
return executable.getParameters().stream()
.map(
v ->
newProperty(
v, identifiers.get(v), propertyToGetterName.get(v.getSimpleName().toString())))
.collect(toImmutableSet());
}
private Property newProperty(VariableElement var, String identifier, String getterName) {
String name = var.getSimpleName().toString();
TypeMirror type = var.asType();
Optional<String> nullableAnnotation = nullableAnnotationFor(var, var.asType());
return new Property(
name, identifier, TypeEncoder.encode(type), type, nullableAnnotation, getterName);
}
private ExecutableElement findExecutable(
TypeElement ofClass,
String callMethod,
TypeElement autoBuilderType,
ImmutableSet<ExecutableElement> methods) {
List<ExecutableElement> executables =
findRelevantExecutables(ofClass, callMethod, autoBuilderType);
String description =
callMethod.isEmpty() ? "constructor" : "static method named \"" + callMethod + "\"";
switch (executables.size()) {
case 0:
throw errorReporter()
.abortWithError(
autoBuilderType,
"[AutoBuilderNoVisible] No visible %s for %s",
description,
ofClass);
case 1:
return executables.get(0);
default:
return matchingExecutable(autoBuilderType, executables, methods, description);
}
}
private ImmutableList<ExecutableElement> findRelevantExecutables(
TypeElement ofClass, String callMethod, TypeElement autoBuilderType) {
List<? extends Element> elements = ofClass.getEnclosedElements();
Stream<ExecutableElement> relevantExecutables =
callMethod.isEmpty()
? constructorsIn(elements).stream()
: methodsIn(elements).stream()
.filter(m -> m.getSimpleName().contentEquals(callMethod))
.filter(m -> m.getModifiers().contains(Modifier.STATIC));
return relevantExecutables
.filter(c -> visibleFrom(c, getPackage(autoBuilderType)))
.collect(toImmutableList());
}
private ExecutableElement matchingExecutable(
TypeElement autoBuilderType,
List<ExecutableElement> executables,
ImmutableSet<ExecutableElement> methods,
String description) {
// There's more than one visible executable (constructor or method). We try to find the one that
// corresponds to the methods in the @AutoBuilder interface. This is a bit approximate. We're
// basically just looking for an executable where all the parameter names correspond to setters
// or property builders in the interface. We might find out after choosing one that it is wrong
// for whatever reason (types don't match, spurious methods, etc). But it is likely that if the
// names are all accounted for in the methods, and if there's no other matching executable with
// more parameters, then this is indeed the one we want. If we later get errors when we try to
// analyze the interface in detail, those are probably legitimate errors and not because we
// picked the wrong executable.
ImmutableList<ExecutableElement> matches =
executables.stream().filter(x -> executableMatches(x, methods)).collect(toImmutableList());
switch (matches.size()) {
case 0:
throw errorReporter()
.abortWithError(
autoBuilderType,
"[AutoBuilderNoMatch] Property names do not correspond to the parameter names of"
+ " any %s:\n%s",
description,
executableListString(executables));
case 1:
return matches.get(0);
default:
// More than one match, let's see if we can find the best one.
}
int max = matches.stream().mapToInt(c -> c.getParameters().size()).max().getAsInt();
ImmutableList<ExecutableElement> maxMatches =
matches.stream().filter(c -> c.getParameters().size() == max).collect(toImmutableList());
if (maxMatches.size() > 1) {
throw errorReporter()
.abortWithError(
autoBuilderType,
"[AutoBuilderAmbiguous] Property names correspond to more than one %s:\n%s",
description,
executableListString(maxMatches));
}
return maxMatches.get(0);
}
private String executableListString(List<ExecutableElement> executables) {
return executables.stream()
.map(AutoBuilderProcessor::executableString)
.collect(joining("\n ", " ", ""));
}
static String executableString(ExecutableElement executable) {
Element nameSource =
executable.getKind() == ElementKind.CONSTRUCTOR
? executable.getEnclosingElement()
: executable;
return nameSource.getSimpleName()
+ executable.getParameters().stream()
.map(v -> v.asType() + " " + v.getSimpleName())
.collect(joining(", ", "(", ")"));
}
private boolean executableMatches(
ExecutableElement executable, ImmutableSet<ExecutableElement> methods) {
// Start with the complete set of parameter names and remove them one by one as we find
// corresponding methods. We ignore case, under the assumption that it is unlikely that a case
// difference is going to allow a candidate to match when another one is better.
// A parameter named foo could be matched by methods like this:
// X foo(Y)
// X setFoo(Y)
// X fooBuilder()
// X fooBuilder(Y)
// There are further constraints, including on the types X and Y, that will later be imposed by
// BuilderMethodClassifier, but here we just require that there be at least one method with
// one of these shapes for foo.
NavigableSet<String> parameterNames =
executable.getParameters().stream()
.map(v -> v.getSimpleName().toString())
.collect(toCollection(() -> new TreeSet<>(String.CASE_INSENSITIVE_ORDER)));
for (ExecutableElement method : methods) {
String name = method.getSimpleName().toString();
if (name.endsWith("Builder")) {
String property = name.substring(0, name.length() - "Builder".length());
parameterNames.remove(property);
}
if (method.getParameters().size() == 1) {
parameterNames.remove(name);
if (name.startsWith("set")) {
parameterNames.remove(name.substring(3));
}
}
if (parameterNames.isEmpty()) {
return true;
}
}
return false;
}
private boolean visibleFrom(Element element, PackageElement fromPackage) {
Visibility visibility = Visibility.effectiveVisibilityOfElement(element);
switch (visibility) {
case PUBLIC:
return true;
case PROTECTED:
// We care about whether the constructor is visible from the generated class. The generated
// class is never going to be a subclass of the class containing the constructor, so
// protected and default access are equivalent.
case DEFAULT:
return getPackage(element).equals(fromPackage);
default:
return false;
}
}
private TypeMirror builtType(ExecutableElement executable) {
switch (executable.getKind()) {
case CONSTRUCTOR:
return executable.getEnclosingElement().asType();
case METHOD:
return executable.getReturnType();
default:
throw new VerifyException("Unexpected executable kind " + executable.getKind());
}
}
private String build(ExecutableElement executable) {
TypeElement enclosing = MoreElements.asType(executable.getEnclosingElement());
String type = TypeEncoder.encodeRaw(enclosing.asType());
switch (executable.getKind()) {
case CONSTRUCTOR:
boolean generic = !enclosing.getTypeParameters().isEmpty();
String typeParams = generic ? "<>" : "";
return "new " + type + typeParams;
case METHOD:
return type + "." + executable.getSimpleName();
default:
throw new VerifyException("Unexpected executable kind " + executable.getKind());
}
}
private static final ElementKind ELEMENT_KIND_RECORD = elementKindRecord();
private static ElementKind elementKindRecord() {
try {
Field record = ElementKind.class.getField("RECORD");
return (ElementKind) record.get(null);
} catch (ReflectiveOperationException e) {
// OK: we must be on a JDK version that predates this.
return null;
}
}
private TypeElement getOfClass(
TypeElement autoBuilderType, AnnotationMirror autoBuilderAnnotation) {
TypeElement ofClassValue = findOfClassValue(autoBuilderAnnotation);
boolean isDefault = typeUtils().isSameType(ofClassValue.asType(), javaLangVoid);
if (!isDefault) {
return ofClassValue;
}
Element enclosing = autoBuilderType.getEnclosingElement();
ElementKind enclosingKind = enclosing.getKind();
if (enclosing.getKind() != ElementKind.CLASS && enclosingKind != ELEMENT_KIND_RECORD) {
errorReporter()
.abortWithError(
autoBuilderType,
"[AutoBuilderEnclosing] @AutoBuilder must specify ofClass=Something.class or it"
+ " must be nested inside the class to be built; actually nested inside %s %s.",
Ascii.toLowerCase(enclosingKind.name()),
enclosing);
}
return MoreElements.asType(enclosing);
}
private TypeElement findOfClassValue(AnnotationMirror autoBuilderAnnotation) {
AnnotationValue ofClassValue =
AnnotationMirrors.getAnnotationValue(autoBuilderAnnotation, "ofClass");
Object value = ofClassValue.getValue();
if (value instanceof TypeMirror) {
TypeMirror ofClassType = (TypeMirror) value;
switch (ofClassType.getKind()) {
case DECLARED:
return MoreTypes.asTypeElement(ofClassType);
case ERROR:
throw new MissingTypeException(MoreTypes.asError(ofClassType));
default:
break;
}
}
throw new MissingTypeException(null);
}
private String findCallMethodValue(AnnotationMirror autoBuilderAnnotation) {
AnnotationValue callMethodValue =
AnnotationMirrors.getAnnotationValue(autoBuilderAnnotation, "callMethod");
return AnnotationValues.getString(callMethodValue);
}
@Override
Optional<String> nullableAnnotationForMethod(ExecutableElement propertyMethod) {
// TODO(b/183005059): implement
return Optional.empty();
}
}