Skip to content

Commit

Permalink
Use reflection to avoid referencing the Kotlin metadata API directly.
Browse files Browse the repository at this point in the history
AutoBuilder clients that don't use Kotlin shouldn't have to have the Kotlin runtime on their classpath, even if it is only the annotation-processing classpath. Additionally, the metadata API was recently moved from `kotlinx.*` to `kotlin.*`, and using reflection means we can function with either version.

Fixes #1440.

PiperOrigin-RevId: 635615798
  • Loading branch information
eamonnmcmanus authored and Google Java Core Libraries committed May 22, 2024
1 parent b21d69d commit be1e359
Show file tree
Hide file tree
Showing 4 changed files with 346 additions and 143 deletions.
28 changes: 0 additions & 28 deletions value/processor/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,6 @@
<groupId>com.squareup</groupId>
<artifactId>javapoet</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-metadata-jvm</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
Expand Down Expand Up @@ -230,21 +225,6 @@
<exclude>com.google.code.findbugs:jsr305</exclude>
</excludes>
</artifactSet>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<!-- Don't include kotlinx-metadata.kotlin_module, etc. We're shading those
libaries and they're only used from Java. Leaving them in the jar creates
"incompatible version" errors from the Kotlin compiler. -->
<exclude>META-INF/*.kotlin_module</exclude>
</excludes>
</filter>
</filters>
<transformers>
<!-- Needed to avoid "No MetadataExtensions instances found in the classpath" from Kotlin reflection. -->
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
</transformers>
<relocations>
<relocation>
<pattern>com.google</pattern>
Expand All @@ -257,14 +237,6 @@
<pattern>com.squareup.javapoet</pattern>
<shadedPattern>autovalue.shaded.com.squareup.javapoet</shadedPattern>
</relocation>
<relocation>
<pattern>kotlin</pattern>
<shadedPattern>autovalue.shaded.kotlin</shadedPattern>
</relocation>
<relocation>
<pattern>kotlinx</pattern>
<shadedPattern>autovalue.shaded.kotlinx</shadedPattern>
</relocation>
<relocation>
<pattern>net.ltgt.gradle.incap</pattern>
<shadedPattern>autovalue.shaded.net.ltgt.gradle.incap</shadedPattern>
Expand Down
16 changes: 15 additions & 1 deletion value/src/it/functional/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,13 @@
</dependency>
</dependencies>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-metadata-jvm</artifactId>
<version>2.0.0</version>
</path>
</annotationProcessorPaths>
<source>${java.specification.version}</source>
<target>${java.specification.version}</target>
<compilerArgs>
Expand Down Expand Up @@ -202,7 +209,7 @@
</goals>
</execution>
</executions>
</plugin>
</plugin>
</plugins>
</build>

Expand All @@ -223,6 +230,13 @@
</dependency>
</dependencies>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-metadata-jvm</artifactId>
<version>2.0.0</version>
</path>
</annotationProcessorPaths>
<source>${java.specification.version}</source>
<target>${java.specification.version}</target>
<compilerArgs>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,9 @@
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;
Expand All @@ -48,7 +46,6 @@
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;
Expand All @@ -72,12 +69,6 @@
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.tools.JavaFileObject;
import kotlinx.metadata.Attributes;
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;

Expand All @@ -104,11 +95,13 @@ public Set<String> getSupportedOptions() {
}

private TypeMirror javaLangVoid;
private KotlinMetadata kotlinMetadata;

@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
javaLangVoid = elementUtils().getTypeElement("java.lang.Void").asType();
kotlinMetadata = new KotlinMetadata(errorReporter());
}

// The handling of @AutoBuilder to generate annotation implementations needs some explanation.
Expand Down Expand Up @@ -207,12 +200,7 @@ private void processType(TypeElement autoBuilderType, TypeElement ofClass, Strin
ImmutableMap<String, String> propertyToGetterName =
propertyToGetterName(executable, autoBuilderType);
AutoBuilderTemplateVars vars = new AutoBuilderTemplateVars();
vars.props =
propertySet(
executable,
propertyToGetterName,
propertyInitializers,
nullables);
vars.props = propertySet(executable, propertyToGetterName, propertyInitializers, nullables);
builder.defineVars(vars, classifier);
vars.identifiers = !processingEnv.getOptions().containsKey(OMIT_IDENTIFIERS_OPTION);
String generatedClassName = generatedClassName(autoBuilderType, "AutoBuilder_");
Expand All @@ -226,8 +214,7 @@ private void processType(TypeElement autoBuilderType, TypeElement ofClass, Strin
.orElseGet(executable::invoke);
vars.toBuilderConstructor = !propertyToGetterName.isEmpty();
vars.toBuilderMethods = ImmutableList.of();
defineSharedVarsForType(
autoBuilderType, ImmutableSet.of(), nullables, vars);
defineSharedVarsForType(autoBuilderType, ImmutableSet.of(), nullables, vars);
String text = vars.toText();
text = TypeEncoder.decode(text, processingEnv, vars.pkg, autoBuilderType.asType());
text = Reformatter.fixup(text);
Expand Down Expand Up @@ -291,8 +278,7 @@ private ImmutableSet<Property> propertySet(
// 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()));
executable.parameters().stream().collect(toMap(v -> v, v -> v.getSimpleName().toString()));
fixReservedIdentifiers(identifiers);
return executable.parameters().stream()
.map(
Expand Down Expand Up @@ -384,8 +370,8 @@ private ImmutableMap<String, String> propertyInitializers(
* <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.
* 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) {
Expand Down Expand Up @@ -464,12 +450,13 @@ private Executable findExecutable(

private ImmutableList<Executable> findRelevantExecutables(
TypeElement ofClass, String callMethod, TypeElement autoBuilderType) {
Optional<AnnotationMirror> kotlinMetadata = kotlinMetadataAnnotation(ofClass);
Optional<AnnotationMirror> kotlinMetadataAnnotation =
kotlinMetadata.kotlinMetadataAnnotation(ofClass);
List<? extends Element> elements = ofClass.getEnclosedElements();
Stream<Executable> relevantExecutables =
callMethod.isEmpty()
? kotlinMetadata
.map(a -> kotlinConstructorsIn(a, ofClass).stream())
? kotlinMetadataAnnotation
.map(a -> kotlinMetadata.kotlinConstructorsIn(a, ofClass).stream())
.orElseGet(() -> constructorsIn(elements).stream().map(Executable::of))
: methodsIn(elements).stream()
.filter(m -> m.getSimpleName().contentEquals(callMethod))
Expand Down Expand Up @@ -527,9 +514,7 @@ private Executable matchingExecutable(
}

private String executableListString(List<Executable> executables) {
return executables.stream()
.map(Object::toString)
.collect(joining("\n ", " ", ""));
return executables.stream().map(Object::toString).collect(joining("\n ", " ", ""));
}

private boolean executableMatches(
Expand Down Expand Up @@ -582,91 +567,6 @@ private boolean visibleFrom(Element element, PackageElement fromPackage) {
}
}

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()
.collect(toImmutableMap(e -> e.getKey().getSimpleName().toString(), e -> e.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.readStrict(header);
KmClass kmClass = classMetadata.getKmClass();
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 (Attributes.getDeclaresDefaultValue(param)) {
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() {
Expand Down Expand Up @@ -780,8 +680,7 @@ private ImmutableSet<Property> annotationBuilderPropertySet(TypeElement annotati
}

private static Property annotationBuilderProperty(
ExecutableElement annotationMethod,
Nullables nullables) {
ExecutableElement annotationMethod, Nullables nullables) {
String name = annotationMethod.getSimpleName().toString();
TypeMirror type = annotationMethod.getReturnType();
return new Property(
Expand Down
Loading

0 comments on commit be1e359

Please sign in to comment.