From 260b61ec7bac0bcbc0b6af571bf80b8ee45119bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89amonn=20McManus?= Date: Tue, 28 May 2024 07:02:15 -0700 Subject: [PATCH] Use reflection to avoid referencing the Kotlin metadata API directly. 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: 637886287 --- value/processor/pom.xml | 28 -- value/src/it/functional/pom.xml | 31 +- .../value/processor/AutoBuilderProcessor.java | 103 +----- .../auto/value/processor/KotlinMetadata.java | 311 ++++++++++++++++++ value/userguide/autobuilder.md | 7 + 5 files changed, 349 insertions(+), 131 deletions(-) create mode 100644 value/src/main/java/com/google/auto/value/processor/KotlinMetadata.java diff --git a/value/processor/pom.xml b/value/processor/pom.xml index 77cf829098..12b3863625 100644 --- a/value/processor/pom.xml +++ b/value/processor/pom.xml @@ -78,11 +78,6 @@ com.squareup javapoet - - org.jetbrains.kotlinx - kotlinx-metadata-jvm - 0.9.0 - org.ow2.asm asm @@ -230,21 +225,6 @@ com.google.code.findbugs:jsr305 - - - *:* - - - META-INF/*.kotlin_module - - - - - - - com.google @@ -257,14 +237,6 @@ com.squareup.javapoet autovalue.shaded.com.squareup.javapoet - - kotlin - autovalue.shaded.kotlin - - - kotlinx - autovalue.shaded.kotlinx - net.ltgt.gradle.incap autovalue.shaded.net.ltgt.gradle.incap diff --git a/value/src/it/functional/pom.xml b/value/src/it/functional/pom.xml index c566043d4f..4685f82d76 100644 --- a/value/src/it/functional/pom.xml +++ b/value/src/it/functional/pom.xml @@ -40,11 +40,6 @@ auto-value-annotations ${project.version} - - com.google.auto.value - auto-value - ${project.version} - com.google.auto.service auto-service @@ -158,6 +153,18 @@ + + + com.google.auto.value + auto-value + ${project.version} + + + org.jetbrains.kotlin + kotlin-metadata-jvm + 2.0.0 + + ${java.specification.version} ${java.specification.version} @@ -202,7 +209,7 @@ - + @@ -223,6 +230,18 @@ + + + com.google.auto.value + auto-value + ${project.version} + + + org.jetbrains.kotlin + kotlin-metadata-jvm + 2.0.0 + + ${java.specification.version} ${java.specification.version} diff --git a/value/src/main/java/com/google/auto/value/processor/AutoBuilderProcessor.java b/value/src/main/java/com/google/auto/value/processor/AutoBuilderProcessor.java index 2d188fd193..9699975cb8 100644 --- a/value/src/main/java/com/google/auto/value/processor/AutoBuilderProcessor.java +++ b/value/src/main/java/com/google/auto/value/processor/AutoBuilderProcessor.java @@ -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; @@ -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; @@ -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; @@ -104,11 +95,13 @@ public Set 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. @@ -464,12 +457,13 @@ private Executable findExecutable( private ImmutableList findRelevantExecutables( TypeElement ofClass, String callMethod, TypeElement autoBuilderType) { - Optional kotlinMetadata = kotlinMetadataAnnotation(ofClass); + Optional kotlinMetadataAnnotation = + kotlinMetadata.kotlinMetadataAnnotation(ofClass); List elements = ofClass.getEnclosedElements(); Stream 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)) @@ -582,91 +576,6 @@ private boolean visibleFrom(Element element, PackageElement fromPackage) { } } - private Optional 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)) - .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 kotlinConstructorsIn( - AnnotationMirror metadata, TypeElement ofClass) { - ImmutableMap 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, ExecutableElement> map = - constructorsIn(ofClass.getEnclosedElements()).stream() - .collect(toMap(c -> parameterNames(c), c -> c, (a, b) -> a, LinkedHashMap::new)); - ImmutableMap, 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 kotlinConstructorsBuilder = ImmutableList.builder(); - for (KmConstructor constructor : kmClass.getConstructors()) { - ImmutableSet.Builder allBuilder = ImmutableSet.builder(); - ImmutableSet.Builder optionalBuilder = ImmutableSet.builder(); - for (KmValueParameter param : constructor.getValueParameters()) { - String name = param.getName(); - allBuilder.add(name); - if (Attributes.getDeclaresDefaultValue(param)) { - optionalBuilder.add(name); - } - } - ImmutableSet optional = optionalBuilder.build(); - ImmutableSet 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 list = (List) value.getValue(); - return list.stream().mapToInt(v -> (int) v.getValue()).toArray(); - } - - private static String[] stringArrayValue(AnnotationValue value) { - @SuppressWarnings("unchecked") - List list = (List) value.getValue(); - return list.stream().map(AnnotationValue::getValue).toArray(String[]::new); - } - - private static ImmutableSet 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() { diff --git a/value/src/main/java/com/google/auto/value/processor/KotlinMetadata.java b/value/src/main/java/com/google/auto/value/processor/KotlinMetadata.java new file mode 100644 index 0000000000..bbe68a67dc --- /dev/null +++ b/value/src/main/java/com/google/auto/value/processor/KotlinMetadata.java @@ -0,0 +1,311 @@ +/* + * Copyright 2024 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.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.ClassNames.KOTLIN_METADATA_NAME; +import static com.google.common.base.Throwables.throwIfUnchecked; +import static java.util.stream.Collectors.toMap; +import static javax.lang.model.util.ElementFilter.constructorsIn; + +import com.google.auto.common.AnnotationMirrors; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +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.TypeElement; + +/** + * Utilities for working with Kotlin metadata. + * + *

We use reflection to avoid referencing the Kotlin metadata API directly. 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. + */ +final class KotlinMetadata { + private final ErrorReporter errorReporter; + private boolean warnedAboutMissingMetadataApi; + + KotlinMetadata(ErrorReporter errorReporter) { + this.errorReporter = errorReporter; + } + + /** + * Use Kotlin reflection to build {@link Executable} instances for the constructors in {@code + * ofClass} that include information about which parameters have default values. + * + * @param metadata the {@code @kotlin.Metadata} annotation on {@code ofClass} + * @param ofClass the class whose constructors should be returned + */ + ImmutableList kotlinConstructorsIn(AnnotationMirror metadata, TypeElement ofClass) { + if (!KOTLIN_METADATA_AVAILABLE) { + if (!warnedAboutMissingMetadataApi) { + warnedAboutMissingMetadataApi = true; + errorReporter.reportWarning( + ofClass, + "[AutoBuilderNoMetadataApi] The Kotlin metadata API (kotlinx.metadata or" + + " kotlin.metadata) is not available. You may need to add a dependency on" + + " org.jetbrains.kotlin:kotlin-metadata-jvm."); + } + return ImmutableList.of(); + } + try { + return kotlinConstructorsFromReflection(metadata, ofClass); + } catch (InvocationTargetException e) { + throwIfUnchecked(e.getCause()); + // We don't expect the Kotlin API to throw checked exceptions. + throw new LinkageError(e.getMessage(), e); + } catch (ReflectiveOperationException e) { + throw new LinkageError(e.getMessage(), e); + } + } + + private static ImmutableList kotlinConstructorsFromReflection( + AnnotationMirror metadata, TypeElement ofClass) throws ReflectiveOperationException { + ImmutableMap 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, ExecutableElement> map = + constructorsIn(ofClass.getEnclosedElements()).stream() + .collect(toMap(c -> parameterNames(c), c -> c, (a, b) -> a, LinkedHashMap::new)); + ImmutableMap, 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.readLenient(header); + KmClass kmClass = classMetadata.getKmClass(); + ImmutableList.Builder kotlinConstructorsBuilder = ImmutableList.builder(); + for (KmConstructor constructor : kmClass.getConstructors()) { + ImmutableSet.Builder allBuilder = ImmutableSet.builder(); + ImmutableSet.Builder optionalBuilder = ImmutableSet.builder(); + for (KmValueParameter param : constructor.getValueParameters()) { + String name = param.getName(); + allBuilder.add(name); + if (Attributes.getDeclaresDefaultValue(param)) { + optionalBuilder.add(name); + } + } + ImmutableSet optional = optionalBuilder.build(); + ImmutableSet all = allBuilder.build(); + ExecutableElement javaConstructor = paramNamesToConstructor.get(all); + if (javaConstructor != null) { + kotlinConstructorsBuilder.add(Executable.of(javaConstructor, optional)); + } + } + return kotlinConstructorsBuilder.build(); + } + + private static ImmutableSet parameterNames(ExecutableElement executableElement) { + return executableElement.getParameters().stream() + .map(v -> v.getSimpleName().toString()) + .collect(toImmutableSet()); + } + + Optional kotlinMetadataAnnotation(Element element) { + return element.getAnnotationMirrors().stream() + .filter( + a -> + asTypeElement(a.getAnnotationType()) + .getQualifiedName() + .contentEquals(KOTLIN_METADATA_NAME)) + .map(a -> a) // get rid of that stupid wildcard + .findFirst(); + } + + private static int[] intArrayValue(AnnotationValue value) { + @SuppressWarnings("unchecked") + List list = (List) value.getValue(); + return list.stream().mapToInt(v -> (int) v.getValue()).toArray(); + } + + private static String[] stringArrayValue(AnnotationValue value) { + @SuppressWarnings("unchecked") + List list = (List) value.getValue(); + return list.stream().map(AnnotationValue::getValue).toArray(String[]::new); + } + + // Wrapper classes for the Kotlin metadata API. These classes have the same names as the ones + // from that API (minus the package of course), and use reflection to access the real API. This + // allows us to write client code that is essentially the same as if we were using the real API. + // Otherwise the logic would be obscured by all the reflective calls. + + private static class KotlinClassHeader { + final Object /* KotlinClassHeader */ wrapped; + + KotlinClassHeader( + Integer k, int[] mv, String[] d1, String[] d2, String xs, String pn, Integer xi) + throws ReflectiveOperationException { + this.wrapped = NEW_KOTLIN_CLASS_HEADER.newInstance(k, mv, d1, d2, xs, pn, xi); + } + } + + @SuppressWarnings({"JavaLangClash", "SameNameButDifferent"}) // "Class" + private static class KotlinClassMetadata { + static Class readLenient(KotlinClassHeader kotlinClassHeader) + throws ReflectiveOperationException { + return new Class( + KOTLIN_CLASS_METADATA_READ_LENIENT.invoke(null, kotlinClassHeader.wrapped)); + } + + static class Class { + final Object /* KotlinClassMetadata.Class */ wrapped; + + Class(Object /* KotlinClassMetadata.Class */ wrapped) { + this.wrapped = wrapped; + } + + KmClass getKmClass() throws ReflectiveOperationException { + return new KmClass(KOTLIN_CLASS_METADATA_CLASS_GET_KM_CLASS.invoke(wrapped)); + } + } + } + + private static class KmClass { + final Object /* KmClass */ wrapped; + + KmClass(Object wrapped) { + this.wrapped = wrapped; + } + + List getConstructors() throws ReflectiveOperationException { + return ((List) KM_CLASS_GET_CONSTRUCTORS.invoke(wrapped)) + .stream().map(KmConstructor::new).collect(toImmutableList()); + } + } + + private static class KmConstructor { + final Object /* KmConstructor */ wrapped; + + KmConstructor(Object wrapped) { + this.wrapped = wrapped; + } + + List getValueParameters() throws ReflectiveOperationException { + return ((List) KM_CONSTRUCTOR_GET_VALUE_PARAMETERS.invoke(wrapped)) + .stream().map(KmValueParameter::new).collect(toImmutableList()); + } + } + + private static class KmValueParameter { + final Object /* KmValueParameter */ wrapped; + + KmValueParameter(Object wrapped) { + this.wrapped = wrapped; + } + + String getName() throws ReflectiveOperationException { + return (String) KM_VALUE_PARAMETER_GET_NAME.invoke(wrapped); + } + } + + private static class Attributes { + private Attributes() {} + + static boolean getDeclaresDefaultValue(KmValueParameter kmValueParameter) + throws ReflectiveOperationException { + return (boolean) ATTRIBUTES_GET_DECLARES_DEFAULT_VALUE.invoke(null, kmValueParameter.wrapped); + } + } + + private static final Constructor NEW_KOTLIN_CLASS_HEADER; + private static final Method KOTLIN_CLASS_METADATA_READ_LENIENT; + private static final Method KOTLIN_CLASS_METADATA_CLASS_GET_KM_CLASS; + private static final Method KM_CLASS_GET_CONSTRUCTORS; + private static final Method KM_CONSTRUCTOR_GET_VALUE_PARAMETERS; + private static final Method KM_VALUE_PARAMETER_GET_NAME; + private static final Method ATTRIBUTES_GET_DECLARES_DEFAULT_VALUE; + private static final boolean KOTLIN_METADATA_AVAILABLE; + + static { + Constructor newKotlinClassHeader = null; + Method kotlinClassMetadataReadLenient = null; + Method kotlinClassMetadataClassGetKmClass = null; + Method kmClassGetConstructors = null; + Method kmConstructorGetValueParameters = null; + Method kmValueParameterGetName = null; + Method attributeGetDeclaresDefaultValue = null; + boolean kotlinMetadataAvailable = false; + for (String prefix : new String[] {"kotlin.metadata.", "kotlinx.metadata."}) { + try { + Class kotlinClassHeaderClass = Class.forName(prefix + "jvm.KotlinClassHeader"); + newKotlinClassHeader = + kotlinClassHeaderClass.getConstructor( + Integer.class, + int[].class, + String[].class, + String[].class, + String.class, + String.class, + Integer.class); + Class kotlinClassMetadataClass = Class.forName(prefix + "jvm.KotlinClassMetadata"); + // Load `kotlin.Metadata` in the same classloader as `kotlinClassHeaderClass`. They are + // potentially from different artifacts so we could otherwise end up with a + // `kotlin.Metadata` that is not actually the type of the `readLenient` parameter because of + // differing classloaders. + Class kotlinMetadataClass = + Class.forName("kotlin.Metadata", false, kotlinClassHeaderClass.getClassLoader()); + kotlinClassMetadataReadLenient = + kotlinClassMetadataClass.getMethod("readLenient", kotlinMetadataClass); + Class kotlinClassMetadataClassClass = + Class.forName(prefix + "jvm.KotlinClassMetadata$Class"); + Class kmClassClass = Class.forName(prefix + "KmClass"); + kotlinClassMetadataClassGetKmClass = kotlinClassMetadataClassClass.getMethod("getKmClass"); + kmClassGetConstructors = kmClassClass.getMethod("getConstructors"); + Class kmConstructorClass = Class.forName(prefix + "KmConstructor"); + kmConstructorGetValueParameters = kmConstructorClass.getMethod("getValueParameters"); + Class kmValueParameterClass = Class.forName(prefix + "KmValueParameter"); + kmValueParameterGetName = kmValueParameterClass.getMethod("getName"); + Class attributeClass = Class.forName(prefix + "Attributes"); + attributeGetDeclaresDefaultValue = + attributeClass.getMethod("getDeclaresDefaultValue", kmValueParameterClass); + kotlinMetadataAvailable = true; + break; + } catch (ReflectiveOperationException e) { + // OK: The metadata API is unavailable with this prefix, and possibly with any prefix. + } + } + NEW_KOTLIN_CLASS_HEADER = newKotlinClassHeader; + KOTLIN_CLASS_METADATA_READ_LENIENT = kotlinClassMetadataReadLenient; + KOTLIN_CLASS_METADATA_CLASS_GET_KM_CLASS = kotlinClassMetadataClassGetKmClass; + KM_CLASS_GET_CONSTRUCTORS = kmClassGetConstructors; + KM_CONSTRUCTOR_GET_VALUE_PARAMETERS = kmConstructorGetValueParameters; + KM_VALUE_PARAMETER_GET_NAME = kmValueParameterGetName; + ATTRIBUTES_GET_DECLARES_DEFAULT_VALUE = attributeGetDeclaresDefaultValue; + KOTLIN_METADATA_AVAILABLE = kotlinMetadataAvailable; + } +} diff --git a/value/userguide/autobuilder.md b/value/userguide/autobuilder.md index 8045192d9d..e427c8865d 100644 --- a/value/userguide/autobuilder.md +++ b/value/userguide/autobuilder.md @@ -119,6 +119,13 @@ The example also implements a `toBuilder()` method to get a builder that starts out with values from the given instance. See [below](#to_builder) for more details on that. +### Required configuration to understand Kotlin classes + +In order for AutoBuilder to understand Kotlin classes, you will typically need +to add a dependency on the `org.jetbrains.kotlin:kotlin-metadata-jvm` package, +in the same place where you depend on `com.google.auto.value:auto-value`. The +older `org.jetbrains.kotlinx:kotlinx-metadata-jvm` should also work. + ## The generated subclass Like `@AutoValue.Builder`, compiling an `@AutoBuilder` class will generate a