blob: 4cc9a3537a9877823bb86cd257ddc196fabc5028 [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.GeneratedAnnotations.generatedAnnotation;
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.toImmutableMap;
import static com.google.auto.common.MoreStreams.toImmutableSet;
import static com.google.auto.common.MoreTypes.asTypeElement;
import static com.google.auto.value.processor.AutoValueProcessor.OMIT_IDENTIFIERS_OPTION;
import static com.google.auto.value.processor.ClassNames.AUTO_ANNOTATION_NAME;
import static com.google.auto.value.processor.ClassNames.AUTO_BUILDER_NAME;
import static com.google.auto.value.processor.ClassNames.KOTLIN_METADATA_NAME;
import static java.util.stream.Collectors.joining;
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.MissingTypes.MissingTypeException;
import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.lang.reflect.Field;
import java.util.AbstractMap.SimpleEntry;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NavigableSet;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
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.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.tools.JavaFileObject;
import kotlinx.metadata.Flag;
import kotlinx.metadata.KmClass;
import kotlinx.metadata.KmConstructor;
import kotlinx.metadata.KmValueParameter;
import kotlinx.metadata.jvm.KotlinClassHeader;
import kotlinx.metadata.jvm.KotlinClassMetadata;
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";
private static final String AUTO_ANNOTATION_CLASS_PREFIX = "AutoBuilderAnnotation_";
public AutoBuilderProcessor() {
super(AUTO_BUILDER_NAME, /* appliesToInterfaces= */ true);
}
@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();
}
// The handling of @AutoBuilder to generate annotation implementations needs some explanation.
// Suppose we have this:
//
// public class Annotations {
// @interface MyAnnot {...}
//
// @AutoBuilder(ofClass = MyAnnot.class)
// public interface MyAnnotBuilder {
// ...
// MyAnnot build();
// }
//
// public static MyAnnotBuilder myAnnotBuilder() {
// return new AutoBuilder_Annotations_MyAnnotBuilder();
// }
// }
//
// Then we will detect that the ofClass type is an annotation. Since annotations can have neither
// constructors nor static methods, we know this isn't a regular @AutoBuilder. We want to
// generate an implementation of the MyAnnot annotation, and we know we can do that if we have a
// suitable @AutoAnnotation method. So we generate:
//
// class AutoBuilderAnnotation_Annotations_MyAnnotBuilder {
// @AutoAnnotation
// static MyAnnot newAnnotation(...) {
// return new AutoAnnotation_AutoBuilderAnnotation_Annotations_MyAnnotBuilder_newAnnotation(
// ...);
// }
// }
//
// We also "defer" MyAnnotBuilder so that it will be considered again on the next round. At that
// point the method AutoBuilderAnnotation_Annotations_MyAnnotBuilder.newAnnotation will exist, and
// we just need to tweak the handling of MyAnnotBuilder so that it behaves as if it were:
//
// @AutoBuilder(
// callMethod = newAnnotation,
// ofClass = AutoBuilderAnnotation_Annotations_MyAnnotBuilder.class)
// interface MyAnnotBuilder {...}
//
// Using AutoAnnotation and AutoBuilder together you'd write
//
// @AutoAnnotation static MyAnnot newAnnotation(...) { ... }
//
// @AutoBuilder(callMethod = "newAnnotation", ofClass = Some.class)
// interface MyAnnotBuilder { ... }
//
// If you set ofClass to an annotation class, AutoBuilder generates the @AutoAnnotation method for
// you and then acts as if your @AutoBuilder annotation pointed to it.
@Override
void processType(TypeElement autoBuilderType) {
if (processingEnv.getOptions().containsKey(ALLOW_OPTION)) {
errorReporter().reportWarning(autoBuilderType, "The -A%s option is obsolete", ALLOW_OPTION);
}
// 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);
if (ofClass.getKind() == ElementKind.ANNOTATION_TYPE) {
buildAnnotation(autoBuilderType, ofClass, callMethod);
} else {
processType(autoBuilderType, ofClass, callMethod);
}
}
private void processType(TypeElement autoBuilderType, TypeElement ofClass, String callMethod) {
ImmutableSet<ExecutableElement> methods =
abstractMethodsIn(
getLocalAndInheritedMethods(autoBuilderType, typeUtils(), elementUtils()));
Executable executable = findExecutable(ofClass, callMethod, autoBuilderType, methods);
BuilderSpec builderSpec = new BuilderSpec(ofClass, processingEnv, errorReporter());
BuilderSpec.Builder builder = builderSpec.new Builder(autoBuilderType);
TypeMirror builtType = executable.builtType();
ImmutableMap<String, String> propertyInitializers =
propertyInitializers(autoBuilderType, executable);
Optional<BuilderMethodClassifier<VariableElement>> maybeClassifier =
BuilderMethodClassifierForAutoBuilder.classify(
methods,
errorReporter(),
processingEnv,
executable,
builtType,
autoBuilderType,
propertyInitializers.keySet());
if (!maybeClassifier.isPresent() || errorReporter().errorCount() > 0) {
// We've already output one or more error messages.
return;
}
BuilderMethodClassifier<VariableElement> classifier = maybeClassifier.get();
ImmutableMap<String, String> propertyToGetterName =
propertyToGetterName(executable, autoBuilderType);
AutoBuilderTemplateVars vars = new AutoBuilderTemplateVars();
vars.props = propertySet(executable, propertyToGetterName, propertyInitializers);
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.builderAnnotations = copiedClassAnnotations(autoBuilderType);
Optional<String> forwardingClassName = maybeForwardingClass(autoBuilderType, executable);
vars.build =
forwardingClassName
.map(n -> TypeSimplifier.simpleNameOf(n) + ".of")
.orElseGet(executable::invoke);
vars.toBuilderConstructor = !propertyToGetterName.isEmpty();
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);
forwardingClassName.ifPresent(
n -> generateForwardingClass(n, executable, builtType, autoBuilderType));
}
/**
* Generates a class that will call the synthetic Kotlin constructor that is used to specify which
* optional parameters are defaulted. Because it is synthetic, it can't be called from Java source
* code. Instead, Java source code calls the {@code of} method in the class we generate here.
*/
private void generateForwardingClass(
String forwardingClassName,
Executable executable,
TypeMirror builtType,
TypeElement autoBuilderType) {
// The synthetic constructor has the same parameters as the user-written constructor, plus as
// many `int` bitmasks as are needed to have one bit for each of those parameters, plus a dummy
// parameter of type kotlin.jvm.internal.DefaultConstructorMarker to avoid confusion with a
// constructor that might have its own `int` parameters where the bitmasks are.
// This ABI is not publicly specified (as far as we know) but JetBrains has confirmed orally
// that it unlikely to change, and if it does it will be in a backward-compatible way.
ImmutableList.Builder<TypeMirror> constructorParameters = ImmutableList.builder();
executable.parameters().stream()
.map(Element::asType)
.map(typeUtils()::erasure)
.forEach(constructorParameters::add);
int bitmaskCount = (executable.optionalParameterCount() + 31) / 32;
constructorParameters.addAll(
Collections.nCopies(bitmaskCount, typeUtils().getPrimitiveType(TypeKind.INT)));
String marker = "kot".concat("lin.jvm.internal.DefaultConstructorMarker"); // defeat shading
constructorParameters.add(elementUtils().getTypeElement(marker).asType());
byte[] classBytes =
ForwardingClassGenerator.makeConstructorForwarder(
forwardingClassName, builtType, constructorParameters.build());
try {
JavaFileObject trampoline =
processingEnv.getFiler().createClassFile(forwardingClassName, autoBuilderType);
try (OutputStream out = trampoline.openOutputStream()) {
out.write(classBytes);
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private Optional<String> maybeForwardingClass(
TypeElement autoBuilderType, Executable executable) {
return executable.optionalParameterCount() == 0
? Optional.empty()
: Optional.of(generatedClassName(autoBuilderType, "AutoBuilderBridge_"));
}
private ImmutableSet<Property> propertySet(
Executable executable,
Map<String, String> propertyToGetterName,
ImmutableMap<String, String> builderInitializers) {
// 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.parameters().stream()
.collect(toMap(v -> v, v -> v.getSimpleName().toString()));
fixReservedIdentifiers(identifiers);
return executable.parameters().stream()
.map(
v -> {
String name = v.getSimpleName().toString();
return newProperty(
v,
identifiers.get(v),
propertyToGetterName.get(name),
Optional.ofNullable(builderInitializers.get(name)),
executable.isOptional(name));
})
.collect(toImmutableSet());
}
private Property newProperty(
VariableElement var,
String identifier,
String getterName,
Optional<String> builderInitializer,
boolean hasDefault) {
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,
builderInitializer,
hasDefault);
}
private ImmutableMap<String, String> propertyInitializers(
TypeElement autoBuilderType, Executable executable) {
boolean autoAnnotation =
MoreElements.getAnnotationMirror(executable.executableElement(), AUTO_ANNOTATION_NAME)
.isPresent();
if (!autoAnnotation) {
return ImmutableMap.of();
}
// We expect the return type of an @AutoAnnotation method to be an annotation type. If it isn't,
// AutoAnnotation will presumably complain, so we don't need to complain further.
TypeMirror returnType = executable.builtType();
if (!returnType.getKind().equals(TypeKind.DECLARED)) {
return ImmutableMap.of();
}
// This might not actually be an annotation (if the code is wrong), but if that's the case we
// just won't see any contained ExecutableElement where getDefaultValue() returns something.
TypeElement annotation = MoreTypes.asTypeElement(returnType);
ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
for (ExecutableElement method : methodsIn(annotation.getEnclosedElements())) {
AnnotationValue defaultValue = method.getDefaultValue();
if (defaultValue != null) {
String memberName = method.getSimpleName().toString();
builder.put(
memberName,
AnnotationOutput.sourceFormForInitializer(
defaultValue, processingEnv, memberName, autoBuilderType));
}
}
return builder.build();
}
/**
* Returns a map from property names to the corresponding getters in the built type. The built
* type is the return type of the given {@code executable}, and the property names are the names
* of its parameters. If the return type is a {@link DeclaredType} {@code Foo} and if every
* property name {@code bar} matches a method {@code bar()} or {@code getBar()} in {@code Foo},
* then the method returns a map where {@code bar} maps to {@code bar} or {@code getBar}. If these
* conditions are not met then the method returns an empty map.
*
* <p>The method name match is case-insensitive, so we will also accept {@code baR()} or {@code
* getbar()}. For a property of type {@code boolean}, we also accept {@code isBar()} (or {@code
* isbar()} etc).
*
* <p>The return type of each getter method must match the type of the corresponding parameter
* exactly. This will always be true for our principal use cases, Java records and Kotlin data
* classes. For other use cases, we may in the future accept getters where we know how to convert,
* for example if the getter has type {@code ImmutableList<Baz>} and the parameter has type
* {@code Baz[]}. We already have similar logic for the parameter types of builder setters.
*/
private ImmutableMap<String, String> propertyToGetterName(
Executable executable, TypeElement autoBuilderType) {
TypeMirror builtType = executable.builtType();
if (builtType.getKind() != TypeKind.DECLARED) {
return ImmutableMap.of();
}
TypeElement type = MoreTypes.asTypeElement(builtType);
Map<String, ExecutableElement> noArgInstanceMethods =
MoreElements.getLocalAndInheritedMethods(type, typeUtils(), elementUtils()).stream()
.filter(m -> m.getParameters().isEmpty())
.filter(m -> !m.getModifiers().contains(Modifier.STATIC))
.filter(m -> visibleFrom(autoBuilderType, getPackage(autoBuilderType)))
.collect(
toMap(
m -> m.getSimpleName().toString(),
m -> m,
(a, b) -> a,
() -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER)));
ImmutableMap<String, String> propertyToGetterName =
executable.parameters().stream()
.map(
param -> {
String name = param.getSimpleName().toString();
// Parameter name is `bar`; we look for `bar()` and `getBar()` (or `getbar()` etc)
// in that order. If `bar` is boolean we also look for `isBar()`.
ExecutableElement getter = noArgInstanceMethods.get(name);
if (getter == null) {
getter = noArgInstanceMethods.get("get" + name);
if (getter == null && param.asType().getKind() == TypeKind.BOOLEAN) {
getter = noArgInstanceMethods.get("is" + name);
}
}
if (getter != null
&& !MoreTypes.equivalence()
.equivalent(getter.getReturnType(), param.asType())) {
getter = null;
}
return new SimpleEntry<>(name, getter);
})
.filter(entry -> entry.getValue() != null)
.collect(
toImmutableMap(
Map.Entry::getKey, entry -> entry.getValue().getSimpleName().toString()));
return (propertyToGetterName.size() == executable.parameters().size())
? propertyToGetterName
: ImmutableMap.of();
}
private Executable findExecutable(
TypeElement ofClass,
String callMethod,
TypeElement autoBuilderType,
ImmutableSet<ExecutableElement> methodsInAutoBuilderType) {
ImmutableList<Executable> 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, methodsInAutoBuilderType, description);
}
}
private ImmutableList<Executable> findRelevantExecutables(
TypeElement ofClass, String callMethod, TypeElement autoBuilderType) {
Optional<AnnotationMirror> kotlinMetadata = kotlinMetadataAnnotation(ofClass);
List<? extends Element> elements = ofClass.getEnclosedElements();
Stream<Executable> relevantExecutables =
callMethod.isEmpty()
? kotlinMetadata
.map(a -> kotlinConstructorsIn(a, ofClass).stream())
.orElseGet(() -> constructorsIn(elements).stream().map(Executable::of))
: methodsIn(elements).stream()
.filter(m -> m.getSimpleName().contentEquals(callMethod))
.filter(m -> m.getModifiers().contains(Modifier.STATIC))
.map(Executable::of);
return relevantExecutables
.filter(e -> visibleFrom(e.executableElement(), getPackage(autoBuilderType)))
.collect(toImmutableList());
}
private Executable matchingExecutable(
TypeElement autoBuilderType,
List<Executable> executables,
ImmutableSet<ExecutableElement> methodsInAutoBuilderType,
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<Executable> matches =
executables.stream()
.filter(x -> executableMatches(x, methodsInAutoBuilderType))
.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(e -> e.parameters().size()).max().getAsInt();
ImmutableList<Executable> maxMatches =
matches.stream().filter(c -> c.parameters().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<Executable> executables) {
return executables.stream()
.map(Object::toString)
.collect(joining("\n ", " ", ""));
}
private boolean executableMatches(
Executable executable, ImmutableSet<ExecutableElement> methodsInAutoBuilderType) {
// 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 = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
parameterNames.addAll(executable.parameterNames());
for (ExecutableElement method : methodsInAutoBuilderType) {
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 Optional<AnnotationMirror> kotlinMetadataAnnotation(Element element) {
// It would be MUCH simpler if we could just use ofClass.getAnnotation(Metadata.class).
// However that would be unsound. We want to shade the Kotlin runtime, including
// kotlin.Metadata, so as not to interfere with other things on the annotation classpath that
// might have a different version of the runtime. That means that if we referenced
// kotlin.Metadata.class here we would actually be referencing
// autovalue.shaded.kotlin.Metadata.class. Obviously the Kotlin class doesn't have that
// annotation.
return element.getAnnotationMirrors().stream()
.filter(
a ->
asTypeElement(a.getAnnotationType())
.getQualifiedName()
.contentEquals(KOTLIN_METADATA_NAME))
.<AnnotationMirror>map(a -> a) // get rid of that stupid wildcard
.findFirst();
}
/**
* Use Kotlin reflection to build {@link Executable} instances for the constructors in {@code
* ofClass} that include information about which parameters have default values.
*/
private ImmutableList<Executable> kotlinConstructorsIn(
AnnotationMirror metadata, TypeElement ofClass) {
ImmutableMap<String, AnnotationValue> annotationValues =
AnnotationMirrors.getAnnotationValuesWithDefaults(metadata).entrySet().stream()
.map(e -> new SimpleEntry<>(e.getKey().getSimpleName().toString(), e.getValue()))
.collect(toImmutableMap(Map.Entry::getKey, Map.Entry::getValue));
// We match the KmConstructor instances with the ExecutableElement instances based on the
// parameter names. We could possibly just assume that the constructors are in the same order.
Map<ImmutableSet<String>, ExecutableElement> map =
constructorsIn(ofClass.getEnclosedElements()).stream()
.collect(toMap(c -> parameterNames(c), c -> c, (a, b) -> a, LinkedHashMap::new));
ImmutableMap<ImmutableSet<String>, ExecutableElement> paramNamesToConstructor =
ImmutableMap.copyOf(map);
KotlinClassHeader header =
new KotlinClassHeader(
(Integer) annotationValues.get("k").getValue(),
intArrayValue(annotationValues.get("mv")),
stringArrayValue(annotationValues.get("d1")),
stringArrayValue(annotationValues.get("d2")),
(String) annotationValues.get("xs").getValue(),
(String) annotationValues.get("pn").getValue(),
(Integer) annotationValues.get("xi").getValue());
KotlinClassMetadata.Class classMetadata =
(KotlinClassMetadata.Class) KotlinClassMetadata.read(header);
KmClass kmClass = classMetadata.toKmClass();
ImmutableList.Builder<Executable> kotlinConstructorsBuilder = ImmutableList.builder();
for (KmConstructor constructor : kmClass.getConstructors()) {
ImmutableSet.Builder<String> allBuilder = ImmutableSet.builder();
ImmutableSet.Builder<String> optionalBuilder = ImmutableSet.builder();
for (KmValueParameter param : constructor.getValueParameters()) {
String name = param.getName();
allBuilder.add(name);
if (Flag.ValueParameter.DECLARES_DEFAULT_VALUE.invoke(param.getFlags())) {
optionalBuilder.add(name);
}
}
ImmutableSet<String> optional = optionalBuilder.build();
ImmutableSet<String> all = allBuilder.build();
ExecutableElement javaConstructor = paramNamesToConstructor.get(all);
if (javaConstructor != null) {
kotlinConstructorsBuilder.add(Executable.of(javaConstructor, optional));
}
}
return kotlinConstructorsBuilder.build();
}
private static int[] intArrayValue(AnnotationValue value) {
@SuppressWarnings("unchecked")
List<AnnotationValue> list = (List<AnnotationValue>) value.getValue();
return list.stream().mapToInt(v -> (int) v.getValue()).toArray();
}
private static String[] stringArrayValue(AnnotationValue value) {
@SuppressWarnings("unchecked")
List<AnnotationValue> list = (List<AnnotationValue>) value.getValue();
return list.stream().map(AnnotationValue::getValue).toArray(String[]::new);
}
private static ImmutableSet<String> parameterNames(ExecutableElement executableElement) {
return executableElement.getParameters().stream()
.map(v -> v.getSimpleName().toString())
.collect(toImmutableSet());
}
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();
}
private void buildAnnotation(
TypeElement autoBuilderType, TypeElement annotationType, String callMethod) {
if (!callMethod.isEmpty()) {
errorReporter()
.abortWithError(
autoBuilderType,
"[AutoBuilderAnnotationMethod] @AutoBuilder for an annotation must have an empty"
+ " callMethod, not \"%s\"",
callMethod);
}
String autoAnnotationClassName =
generatedClassName(autoBuilderType, AUTO_ANNOTATION_CLASS_PREFIX);
TypeElement autoAnnotationClass = elementUtils().getTypeElement(autoAnnotationClassName);
if (autoAnnotationClass != null) {
processType(autoBuilderType, autoAnnotationClass, "newAnnotation");
return;
}
AutoBuilderAnnotationTemplateVars vars = new AutoBuilderAnnotationTemplateVars();
vars.autoBuilderType = TypeEncoder.encode(autoBuilderType.asType());
vars.props = annotationBuilderPropertySet(annotationType);
vars.pkg = TypeSimplifier.packageNameOf(autoBuilderType);
vars.generated =
generatedAnnotation(elementUtils(), processingEnv.getSourceVersion())
.map(annotation -> TypeEncoder.encode(annotation.asType()))
.orElse("");
vars.className = TypeSimplifier.simpleNameOf(autoAnnotationClassName);
vars.annotationType = TypeEncoder.encode(annotationType.asType());
String text = vars.toText();
text = TypeEncoder.decode(text, processingEnv, vars.pkg, /* baseType= */ javaLangVoid);
text = Reformatter.fixup(text);
writeSourceFile(autoAnnotationClassName, text, autoBuilderType);
addDeferredType(autoBuilderType);
}
private ImmutableSet<Property> annotationBuilderPropertySet(TypeElement annotationType) {
// Translate the annotation elements into fake Property instances. We're really only interested
// in the name and type, so we can use them to declare a parameter of the generated
// @AutoAnnotation method. We'll generate a parameter for every element, even elements that
// don't have setters in the builder. The generated builder implementation will pass the default
// value from the annotation to those parameters.
return methodsIn(annotationType.getEnclosedElements()).stream()
.filter(m -> m.getParameters().isEmpty() && !m.getModifiers().contains(Modifier.STATIC))
.map(AutoBuilderProcessor::annotationBuilderProperty)
.collect(toImmutableSet());
}
private static Property annotationBuilderProperty(ExecutableElement annotationMethod) {
String name = annotationMethod.getSimpleName().toString();
TypeMirror type = annotationMethod.getReturnType();
return new Property(
name,
name,
TypeEncoder.encode(type),
type,
/* nullableAnnotation= */ Optional.empty(),
/* getter= */ "",
/* maybeBuilderInitializer= */ Optional.empty(),
/* hasDefault= */ false);
}
}