From f8f0decde451e66e31951865721c3dddf0065c3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89amonn=20McManus?= Date: Mon, 14 Aug 2023 16:48:50 -0700 Subject: [PATCH] Access the Kotlin metadata API through reflection. This means we don't need a dependency on that API when building AutoValue. Therefore users can freely supply the version of the API that corresponds to the actual Kotlin version they are using. The downside is that they may need to add that dependency explicitly when previously they were getting it through AutoValue. (But possibly getting the wrong version.) Fixes #1574. RELNOTES=AutoValue no longer has an explicit dependency on the Kotlin Metadata API. **You may need to add an explicit dependency on `org.jetbrains.kotlinx:kotlinx-metadata-jvm.** The reason for this change is to avoid problems that occurred when the version of the API that AutoValue bundles was different from the version that the user code expected. See #1574. PiperOrigin-RevId: 556951184 --- value/processor/pom.xml | 5 - value/src/it/functional/pom.xml | 5 + .../value/processor/AutoBuilderProcessor.java | 99 +------ .../auto/value/processor/KotlinMetadata.java | 279 ++++++++++++++++++ 4 files changed, 287 insertions(+), 101 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 cedc5f0824..72ccb0f03e 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.7.0 - org.ow2.asm asm diff --git a/value/src/it/functional/pom.xml b/value/src/it/functional/pom.xml index 3babf99105..968582b1fb 100644 --- a/value/src/it/functional/pom.xml +++ b/value/src/it/functional/pom.xml @@ -105,6 +105,11 @@ kotlin-stdlib ${kotlin.version} + + org.jetbrains.kotlinx + kotlinx-metadata-jvm + 0.7.0 + 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 607e7367b9..29f1aed37c 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.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; @@ -457,12 +448,13 @@ private Executable findExecutable( private ImmutableList findRelevantExecutables( TypeElement ofClass, String callMethod, TypeElement autoBuilderType) { - Optional kotlinMetadata = kotlinMetadataAnnotation(ofClass); + Optional kotlinMetadata = + KotlinMetadata.kotlinMetadataAnnotation(ofClass); List elements = ofClass.getEnclosedElements(); Stream relevantExecutables = callMethod.isEmpty() ? kotlinMetadata - .map(a -> kotlinConstructorsIn(a, ofClass).stream()) + .map(a -> KotlinMetadata.kotlinConstructorsIn(errorReporter(), a, ofClass).stream()) .orElseGet(() -> constructorsIn(elements).stream().map(Executable::of)) : methodsIn(elements).stream() .filter(m -> m.getSimpleName().contentEquals(callMethod)) @@ -575,91 +567,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.read(header); - KmClass kmClass = classMetadata.toKmClass(); - 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 (Flag.ValueParameter.DECLARES_DEFAULT_VALUE.invoke(param.getFlags())) { - 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..e7e0915ab9 --- /dev/null +++ b/value/src/main/java/com/google/auto/value/processor/KotlinMetadata.java @@ -0,0 +1,279 @@ +/* + * Copyright 2023 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.toImmutableSet; +import static java.util.stream.Collectors.toMap; +import static javax.lang.model.util.ElementFilter.constructorsIn; + +import com.google.common.base.VerifyException; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.reflect.Reflection; +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; + +/** + * Reflective access to Kotlin metadata. + * + *

We use Java reflection to access the Kotlin metadata API, so that we don't need a compile-time + * dependency on that API. This means that AutoValue can ship without a dependency on a particular + * version of the API, so projects can use AutoValue with whatever version of the API suits them. + */ +final class KotlinMetadata { + /** The {@code kotlin.Metadata} annotation class. */ + private static final Class KOTLIN_METADATA_ANNOTATION; + + /** The {@code kotlinx.metadata.jvm.KotlinClassHeader} constructor. */ + private static final Constructor KOTLIN_CLASS_HEADER_CONSTRUCTOR; + + /** The {@code kotlinx.metadata.jvm.KotlinClassMetadata.read(kotlin.Metadata)} method. */ + private static final Method KOTLIN_CLASS_METADATA_READ; + + /** The {@code kotlinx.metadata.jvm.KotlinClassMetadata.Class.toKmClass()} method. */ + private static final Method KOTLIN_CLASS_METADATA_CLASS_TO_KM_CLASS; + + /** The {@code kotlinx.metadata.KmClass.getConstructors()} method. */ + private static final Method KM_CLASS_GET_CONSTRUCTORS; + + /** The {@code kotlinx.metadata.KmConstructor.getValueParameters()} method. */ + private static final Method KM_CONSTRUCTOR_GET_VALUE_PARAMETERS; + + /** The {@code kotlinx.metadata.KmValueParameter.getName()} method. */ + private static final Method KM_VALUE_PARAMETER_GET_NAME; + + /** The {@code kotlinx.metadata.KmValueParameter.getFlags()} method. */ + private static final Method KM_VALUE_PARAMETER_GET_FLAGS; + + /** The {@code kotlinx.metadata.Flag.ValueParameter.DECLARES_DEFAULT_VALUE} field. */ + private static final Field FLAG_VALUE_PARAMETER_DECLARES_DEFAULT_VALUE; + + /** The {@code kotlinx.metadata.Flag.invoke} method. */ + private static final Method FLAG_INVOKE; + + /** An exception that may have occurred while trying to look up any of the above. */ + private static final ReflectiveOperationException KOTLIN_API_REFLECTIVE_OPERATION_EXCEPTION; + + static { + Class kotlinMetadataAnnotation = null; + Constructor newKotlinClassHeader = null; + Method kotlinClassMetadataRead = null; + Method kotlinClassMetadataClassToKmClass = null; + Method kmClassGetConstructors = null; + Method kmConstuctorGetValueParameters = null; + Method kmValueParameterGetName = null; + Method kmValueParameterGetFlags = null; + Field flagValueParameterDeclaresDefaultValue = null; + Method flagInvoke = null; + boolean shouldWork = false; + ReflectiveOperationException kotlinApiReflectiveOperationException = null; + try { + kotlinMetadataAnnotation = Class.forName("kotlin.Metadata").asSubclass(Annotation.class); + Class kotlinClassHeader = Class.forName("kotlinx.metadata.jvm.KotlinClassHeader"); + shouldWork = true; // If we get the above but not the below, something is wrong. + newKotlinClassHeader = + kotlinClassHeader.getConstructor( + Integer.class, + int[].class, + String[].class, + String[].class, + String.class, + String.class, + Integer.class); + Class kotlinClassMetadata = Class.forName("kotlinx.metadata.jvm.KotlinClassMetadata"); + Class kotlinMetadata = Class.forName("kotlin.Metadata"); + kotlinClassMetadataRead = kotlinClassMetadata.getMethod("read", kotlinMetadata); + Class kotlinClassMetadataClass = + Class.forName("kotlinx.metadata.jvm.KotlinClassMetadata$Class"); + kotlinClassMetadataClassToKmClass = kotlinClassMetadataClass.getMethod("toKmClass"); + Class kmClass = Class.forName("kotlinx.metadata.KmClass"); + kmClassGetConstructors = kmClass.getMethod("getConstructors"); + Class kmConstuctor = Class.forName("kotlinx.metadata.KmConstructor"); + kmConstuctorGetValueParameters = kmConstuctor.getMethod("getValueParameters"); + Class kmValueParameter = Class.forName("kotlinx.metadata.KmValueParameter"); + kmValueParameterGetName = kmValueParameter.getMethod("getName"); + kmValueParameterGetFlags = kmValueParameter.getMethod("getFlags"); + Class flagValueParameter = Class.forName("kotlinx.metadata.Flag$ValueParameter"); + flagValueParameterDeclaresDefaultValue = + flagValueParameter.getField("DECLARES_DEFAULT_VALUE"); + Class flag = Class.forName("kotlinx.metadata.Flag"); + flagInvoke = flag.getMethod("invoke", int.class); + } catch (ReflectiveOperationException e) { + if (shouldWork) { + kotlinApiReflectiveOperationException = e; + } + } + KOTLIN_METADATA_ANNOTATION = kotlinMetadataAnnotation; + KOTLIN_CLASS_HEADER_CONSTRUCTOR = newKotlinClassHeader; + KOTLIN_CLASS_METADATA_READ = kotlinClassMetadataRead; + KOTLIN_CLASS_METADATA_CLASS_TO_KM_CLASS = kotlinClassMetadataClassToKmClass; + KM_CLASS_GET_CONSTRUCTORS = kmClassGetConstructors; + KM_CONSTRUCTOR_GET_VALUE_PARAMETERS = kmConstuctorGetValueParameters; + KM_VALUE_PARAMETER_GET_NAME = kmValueParameterGetName; + KM_VALUE_PARAMETER_GET_FLAGS = kmValueParameterGetFlags; + FLAG_VALUE_PARAMETER_DECLARES_DEFAULT_VALUE = flagValueParameterDeclaresDefaultValue; + FLAG_INVOKE = flagInvoke; + KOTLIN_API_REFLECTIVE_OPERATION_EXCEPTION = kotlinApiReflectiveOperationException; + } + + /** + * A copy of the Java equivalent of {@code kotlin.Metadata} which we will access through a {@link + * Proxy}. + */ + interface KotlinMetadataAnnotation { + int k(); + + int[] mv(); + + String[] d1(); + + String[] d2(); + + String xs(); + + String pn(); + + int xi(); + } + + /** + * Returns an implementation of {@link KotlinMetadataAnnotation} that forwards its methods to the + * given annotation instance. It is expected that that instance has the same methods, or a + * superset of them. + */ + private static KotlinMetadataAnnotation annotationProxy(Annotation annotation) { + InvocationHandler invocationHandler = + (unusedProxy, method, args) -> { + try { + Method annotationMethod = annotation.annotationType().getMethod(method.getName()); + return annotationMethod.invoke(annotation, args); + } catch (ReflectiveOperationException e) { + throw new VerifyException(e); + } + }; + return Reflection.newProxy(KotlinMetadataAnnotation.class, invocationHandler); + } + + static final AtomicBoolean complained = new AtomicBoolean(false); + + /** Returns an equivalent of the {@code kotlin.Metadata} on the given element, if there is one. */ + static Optional kotlinMetadataAnnotation(Element element) { + if (KOTLIN_METADATA_ANNOTATION == null) { + return Optional.empty(); + } + Annotation annotation = element.getAnnotation(KOTLIN_METADATA_ANNOTATION); + return Optional.ofNullable(annotation).map(KotlinMetadata::annotationProxy); + } + + /** Returns a list of the Kotlin constructors in {@code ofClass}. */ + static ImmutableList kotlinConstructorsIn( + ErrorReporter errorReporter, KotlinMetadataAnnotation metadata, TypeElement ofClass) { + // 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); + try { + if (KOTLIN_API_REFLECTIVE_OPERATION_EXCEPTION != null) { + // We weren't able to get all the Methods etc, so complain if we haven't already. + throw KOTLIN_API_REFLECTIVE_OPERATION_EXCEPTION; + } + return kotlinConstructorsIn(metadata, paramNamesToConstructor); + } catch (ReflectiveOperationException e) { + if (!complained.getAndSet(true)) { + errorReporter.reportWarning(ofClass, "Exception reading Kotlin metadata: %s", e); + } + return ImmutableList.of(); + } + } + + private static ImmutableList kotlinConstructorsIn( + KotlinMetadataAnnotation metadata, + ImmutableMap, ExecutableElement> paramNamesToConstructor) + throws ReflectiveOperationException { + // header = new KotlinClassHeader(...); + Object header = + KOTLIN_CLASS_HEADER_CONSTRUCTOR.newInstance( + metadata.k(), + metadata.mv(), + metadata.d1(), + metadata.d2(), + metadata.xs(), + metadata.pn(), + metadata.xi()); + + // KotlinClassMetadata.Class classMetadata = KotlinClassMetadata.read(header); + Object classMetadata = KOTLIN_CLASS_METADATA_READ.invoke(null, header); + + // KmClass kmClass = classMetadata.toKmClass(); + Object kmClass = KOTLIN_CLASS_METADATA_CLASS_TO_KM_CLASS.invoke(classMetadata); + + // List kmConstructors = kmClass.getConstructors() + List kmConstructors = (List) KM_CLASS_GET_CONSTRUCTORS.invoke(kmClass); + + ImmutableList.Builder kotlinConstructorsBuilder = ImmutableList.builder(); + for (Object kmConstructor : kmConstructors) { + ImmutableSet.Builder allBuilder = ImmutableSet.builder(); + ImmutableSet.Builder optionalBuilder = ImmutableSet.builder(); + + // List params = kmConstructor.getValueParameters(); + List params = (List) KM_CONSTRUCTOR_GET_VALUE_PARAMETERS.invoke(kmConstructor); + for (Object param : params) { + String name = (String) KM_VALUE_PARAMETER_GET_NAME.invoke(param); + allBuilder.add(name); + + // Flag flag = Flag.ValueParameter.DECLARES_DEFAULT_VALUE + Object flag = FLAG_VALUE_PARAMETER_DECLARES_DEFAULT_VALUE.get(null); + // if (flag.invoke(param.getFlags()) ... + int flags = (Integer) KM_VALUE_PARAMETER_GET_FLAGS.invoke(param); + if ((Boolean) FLAG_INVOKE.invoke(flag, flags)) { + 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()); + } + + private KotlinMetadata() {} +}