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 find either version.

Fixes #1440.

RELNOTES=AutoValue (including AutoBuilder) no longer bundles the Kotlin runtime. This may require adding an explicit dependency, though usually if the Kotlin runtime is needed it is because you are either compiling Kotlin or compiling Java code that depends on Kotlin code. Either way you should have a transitive dependency on the Kotlin runtime.
PiperOrigin-RevId: 635615798
  • Loading branch information
eamonnmcmanus authored and Google Java Core Libraries committed May 21, 2024
1 parent 29f739b commit e201492
Show file tree
Hide file tree
Showing 4 changed files with 232 additions and 119 deletions.
23 changes: 0 additions & 23 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 @@ -233,18 +228,8 @@
<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 +242,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
5 changes: 5 additions & 0 deletions value/src/it/functional/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@
<version>8.6</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlinx-metadata-jvm</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>com.google.escapevelocity</groupId>
<artifactId>escapevelocity</artifactId>
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.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;

Expand Down Expand Up @@ -464,12 +455,12 @@ private Executable findExecutable(

private ImmutableList<Executable> findRelevantExecutables(
TypeElement ofClass, String callMethod, TypeElement autoBuilderType) {
Optional<AnnotationMirror> kotlinMetadata = kotlinMetadataAnnotation(ofClass);
Optional<AnnotationMirror> kotlinMetadata = KotlinMetadata.kotlinMetadataAnnotation(ofClass);
List<? extends Element> elements = ofClass.getEnclosedElements();
Stream<Executable> relevantExecutables =
callMethod.isEmpty()
? kotlinMetadata
.map(a -> kotlinConstructorsIn(a, ofClass).stream())
.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 @@ -582,91 +573,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.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() {
Expand Down
Loading

0 comments on commit e201492

Please sign in to comment.