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