diff options
Diffstat (limited to 'interop/kotlinx-metadata/src/main/kotlin')
19 files changed, 4060 insertions, 0 deletions
diff --git a/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/Flags.kt b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/Flags.kt new file mode 100644 index 00000000..4745dda0 --- /dev/null +++ b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/Flags.kt @@ -0,0 +1,338 @@ +/* + * Copyright (C) 2019 Square, Inc. + * + * 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 + * + * https://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. + */ +@file:Suppress("unused") + +package com.squareup.kotlinpoet.metadata + +import kotlinx.metadata.Flag +import kotlinx.metadata.Flags +import kotlinx.metadata.KmClass +import kotlinx.metadata.KmConstructor +import kotlinx.metadata.KmFunction +import kotlinx.metadata.KmProperty +import kotlinx.metadata.KmType +import kotlinx.metadata.KmTypeParameter +import kotlinx.metadata.KmValueParameter + +// Common flags for any element with flags. +@KotlinPoetMetadataPreview +public val Flags.hasAnnotations: Boolean get() = Flag.HAS_ANNOTATIONS(this) + +@KotlinPoetMetadataPreview +public val Flags.isAbstract: Boolean get() = Flag.IS_ABSTRACT(this) + +@KotlinPoetMetadataPreview +public val Flags.isFinal: Boolean get() = Flag.IS_FINAL(this) + +@KotlinPoetMetadataPreview +public val Flags.isInternal: Boolean get() = Flag.IS_INTERNAL(this) + +@KotlinPoetMetadataPreview +public val Flags.isLocal: Boolean get() = Flag.IS_LOCAL(this) + +@KotlinPoetMetadataPreview +public val Flags.isOpen: Boolean get() = Flag.IS_OPEN(this) + +@KotlinPoetMetadataPreview +public val Flags.isPrivate: Boolean get() = Flag.IS_PRIVATE(this) + +@KotlinPoetMetadataPreview +public val Flags.isPrivate_to_this: Boolean get() = Flag.IS_PRIVATE_TO_THIS(this) + +@KotlinPoetMetadataPreview +public val Flags.isProtected: Boolean get() = Flag.IS_PROTECTED(this) + +@KotlinPoetMetadataPreview +public val Flags.isPublic: Boolean get() = Flag.IS_PUBLIC(this) + +@KotlinPoetMetadataPreview +public val Flags.isSealed: Boolean get() = Flag.IS_SEALED(this) + +// Type flags. +@KotlinPoetMetadataPreview +public val Flags.isNullableType: Boolean get() = Flag.Type.IS_NULLABLE(this) + +@KotlinPoetMetadataPreview +public val Flags.isSuspendType: Boolean get() = Flag.Type.IS_SUSPEND(this) + +// Class flags. +@KotlinPoetMetadataPreview +public val Flags.isAnnotationClass: Boolean get() = Flag.Class.IS_ANNOTATION_CLASS(this) + +@KotlinPoetMetadataPreview +public val Flags.isClass: Boolean get() = Flag.Class.IS_CLASS(this) + +@KotlinPoetMetadataPreview +public val Flags.isCompanionObjectClass: Boolean get() = Flag.Class.IS_COMPANION_OBJECT(this) + +@KotlinPoetMetadataPreview +public val Flags.isDataClass: Boolean get() = Flag.Class.IS_DATA(this) + +@KotlinPoetMetadataPreview +public val Flags.isEnumClass: Boolean get() = Flag.Class.IS_ENUM_CLASS(this) + +@KotlinPoetMetadataPreview +public val Flags.isEnumEntryClass: Boolean get() = Flag.Class.IS_ENUM_ENTRY(this) + +@KotlinPoetMetadataPreview +public val Flags.isExpectClass: Boolean get() = Flag.Class.IS_EXPECT(this) + +@KotlinPoetMetadataPreview +public val Flags.isExternalClass: Boolean get() = Flag.Class.IS_EXTERNAL(this) + +@KotlinPoetMetadataPreview +public val Flags.isValueClass: Boolean get() = Flag.Class.IS_VALUE(this) + +@KotlinPoetMetadataPreview +public val Flags.isInnerClass: Boolean get() = Flag.Class.IS_INNER(this) + +@KotlinPoetMetadataPreview +public val Flags.isObjectClass: Boolean get() = Flag.Class.IS_OBJECT(this) + +@KotlinPoetMetadataPreview +public val Flags.isInterface: Boolean get() = Flag.Class.IS_INTERFACE(this) + +@KotlinPoetMetadataPreview +public val Flags.isFun: Boolean get() = Flag.Class.IS_FUN(this) + +@KotlinPoetMetadataPreview +public val KmClass.isAnnotation: Boolean get() = flags.isAnnotationClass + +@KotlinPoetMetadataPreview +public val KmClass.isClass: Boolean get() = flags.isClass + +@KotlinPoetMetadataPreview +public val KmClass.isCompanionObject: Boolean get() = flags.isCompanionObjectClass + +@KotlinPoetMetadataPreview +public val KmClass.isData: Boolean get() = flags.isDataClass + +@KotlinPoetMetadataPreview +public val KmClass.isEnum: Boolean get() = flags.isEnumClass + +@KotlinPoetMetadataPreview +public val KmClass.isEnumEntry: Boolean get() = flags.isEnumEntryClass + +@KotlinPoetMetadataPreview +public val KmClass.isExpect: Boolean get() = flags.isExpectClass + +@KotlinPoetMetadataPreview +public val KmClass.isExternal: Boolean get() = flags.isExternalClass + +@KotlinPoetMetadataPreview +public val KmClass.isValue: Boolean get() = flags.isValueClass + +@KotlinPoetMetadataPreview +public val KmClass.isInner: Boolean get() = flags.isInnerClass + +@KotlinPoetMetadataPreview +public val KmClass.isObject: Boolean get() = flags.isObjectClass + +@KotlinPoetMetadataPreview +public val KmClass.isInterface: Boolean get() = flags.isInterface + +@KotlinPoetMetadataPreview +public val KmClass.isFun: Boolean get() = flags.isFun + +@KotlinPoetMetadataPreview +public val KmType.isSuspend: Boolean get() = flags.isSuspendType + +@KotlinPoetMetadataPreview +public val KmType.isNullable: Boolean get() = flags.isNullableType + +// Constructor flags. +@KotlinPoetMetadataPreview +public val Flags.isPrimaryConstructor: Boolean get() = !Flag.Constructor.IS_SECONDARY(this) + +@KotlinPoetMetadataPreview +public val KmConstructor.isPrimary: Boolean get() = flags.isPrimaryConstructor + +@KotlinPoetMetadataPreview +public val KmConstructor.isSecondary: Boolean get() = !isPrimary + +// Function flags. +@KotlinPoetMetadataPreview +public val Flags.isDeclarationFunction: Boolean get() = Flag.Function.IS_DECLARATION(this) + +@KotlinPoetMetadataPreview +public val Flags.isFakeOverrideFunction: Boolean get() = Flag.Function.IS_FAKE_OVERRIDE(this) + +@KotlinPoetMetadataPreview +public val Flags.isDelegationFunction: Boolean get() = Flag.Function.IS_DELEGATION(this) + +@KotlinPoetMetadataPreview +public val Flags.isSynthesizedFunction: Boolean get() = Flag.Function.IS_SYNTHESIZED(this) + +@KotlinPoetMetadataPreview +public val Flags.isOperatorFunction: Boolean get() = Flag.Function.IS_OPERATOR(this) + +@KotlinPoetMetadataPreview +public val Flags.isInfixFunction: Boolean get() = Flag.Function.IS_INFIX(this) + +@KotlinPoetMetadataPreview +public val Flags.isInlineFunction: Boolean get() = Flag.Function.IS_INLINE(this) + +@KotlinPoetMetadataPreview +public val Flags.isTailRecFunction: Boolean get() = Flag.Function.IS_TAILREC(this) + +@KotlinPoetMetadataPreview +public val Flags.isExternalFunction: Boolean get() = Flag.Function.IS_EXTERNAL(this) + +@KotlinPoetMetadataPreview +public val Flags.isSuspendFunction: Boolean get() = Flag.Function.IS_SUSPEND(this) + +@KotlinPoetMetadataPreview +public val Flags.isExpectFunction: Boolean get() = Flag.Function.IS_EXPECT(this) + +@KotlinPoetMetadataPreview +public val KmFunction.isDeclaration: Boolean get() = flags.isDeclarationFunction + +@KotlinPoetMetadataPreview +public val KmFunction.isFakeOverride: Boolean get() = flags.isFakeOverrideFunction + +@KotlinPoetMetadataPreview +public val KmFunction.isDelegation: Boolean get() = flags.isDelegationFunction + +@KotlinPoetMetadataPreview +public val KmFunction.isSynthesized: Boolean get() = flags.isSynthesizedFunction + +@KotlinPoetMetadataPreview +public val KmFunction.isOperator: Boolean get() = flags.isOperatorFunction + +@KotlinPoetMetadataPreview +public val KmFunction.isInfix: Boolean get() = flags.isInfixFunction + +@KotlinPoetMetadataPreview +public val KmFunction.isInline: Boolean get() = flags.isInlineFunction + +@KotlinPoetMetadataPreview +public val KmFunction.isTailRec: Boolean get() = flags.isTailRecFunction + +@KotlinPoetMetadataPreview +public val KmFunction.isExternal: Boolean get() = flags.isExternalFunction + +@KotlinPoetMetadataPreview +public val KmFunction.isSuspend: Boolean get() = flags.isSuspendFunction + +@KotlinPoetMetadataPreview +public val KmFunction.isExpect: Boolean get() = flags.isExpectFunction + +// Parameter flags. +@KotlinPoetMetadataPreview +public val KmValueParameter.declaresDefaultValue: Boolean get() = + Flag.ValueParameter.DECLARES_DEFAULT_VALUE(flags) + +@KotlinPoetMetadataPreview +public val KmValueParameter.isCrossInline: Boolean get() = Flag.ValueParameter.IS_CROSSINLINE(flags) + +@KotlinPoetMetadataPreview +public val KmValueParameter.isNoInline: Boolean get() = Flag.ValueParameter.IS_NOINLINE(flags) + +// Property flags. +@KotlinPoetMetadataPreview +public val Flags.isFakeOverrideProperty: Boolean get() = Flag.Property.IS_FAKE_OVERRIDE(this) + +@KotlinPoetMetadataPreview +public val KmProperty.hasConstant: Boolean get() = Flag.Property.HAS_CONSTANT(flags) + +@KotlinPoetMetadataPreview +public val KmProperty.hasGetter: Boolean get() = Flag.Property.HAS_GETTER(flags) + +@KotlinPoetMetadataPreview +public val KmProperty.hasSetter: Boolean get() = Flag.Property.HAS_SETTER(flags) + +@KotlinPoetMetadataPreview +public val KmProperty.isConst: Boolean get() = Flag.Property.IS_CONST(flags) + +@KotlinPoetMetadataPreview +public val KmProperty.isDeclaration: Boolean get() = Flag.Property.IS_DECLARATION(flags) + +@KotlinPoetMetadataPreview +public val KmProperty.isDelegated: Boolean get() = Flag.Property.IS_DELEGATED(flags) + +@KotlinPoetMetadataPreview +public val KmProperty.isDelegation: Boolean get() = Flag.Property.IS_DELEGATION(flags) + +@KotlinPoetMetadataPreview +public val KmProperty.isExpect: Boolean get() = Flag.Property.IS_EXPECT(flags) + +@KotlinPoetMetadataPreview +public val KmProperty.isExternal: Boolean get() = Flag.Property.IS_EXTERNAL(flags) + +@KotlinPoetMetadataPreview +public val KmProperty.isFakeOverride: Boolean get() = flags.isFakeOverrideProperty + +@KotlinPoetMetadataPreview +public val KmProperty.isLateinit: Boolean get() = Flag.Property.IS_LATEINIT(flags) + +@KotlinPoetMetadataPreview +public val KmProperty.isSynthesized: Boolean get() = Flag.Property.IS_SYNTHESIZED(flags) + +@KotlinPoetMetadataPreview +public val KmProperty.isVar: Boolean get() = Flag.Property.IS_VAR(flags) + +@KotlinPoetMetadataPreview +public val KmProperty.isVal: Boolean get() = !isVar + +// Property Accessor Flags +@KotlinPoetMetadataPreview +public val Flags.isPropertyAccessorExternal: Boolean + get() = Flag.PropertyAccessor.IS_EXTERNAL(this) + +@KotlinPoetMetadataPreview +public val Flags.isPropertyAccessorInline: Boolean + get() = Flag.PropertyAccessor.IS_INLINE(this) + +@KotlinPoetMetadataPreview +public val Flags.isPropertyAccessorNotDefault: Boolean + get() = Flag.PropertyAccessor.IS_NOT_DEFAULT(this) + +// TypeParameter flags. +@KotlinPoetMetadataPreview +public val KmTypeParameter.isReified: Boolean get() = Flag.TypeParameter.IS_REIFIED(flags) + +// Property Accessor Flags +public enum class PropertyAccessorFlag { + IS_EXTERNAL, + IS_INLINE, + IS_NOT_DEFAULT, +} + +@KotlinPoetMetadataPreview +public val KmProperty.setterPropertyAccessorFlags: Set<PropertyAccessorFlag> + get() = setterFlags.propertyAccessorFlags + +@KotlinPoetMetadataPreview +public val KmProperty.getterPropertyAccessorFlags: Set<PropertyAccessorFlag> + get() = getterFlags.propertyAccessorFlags + +@KotlinPoetMetadataPreview +public val Flags.propertyAccessorFlags: Set<PropertyAccessorFlag> + get() = setOf { + if (Flag.PropertyAccessor.IS_EXTERNAL(this@propertyAccessorFlags)) { + add(PropertyAccessorFlag.IS_EXTERNAL) + } + if (Flag.PropertyAccessor.IS_INLINE(this@propertyAccessorFlags)) { + add(PropertyAccessorFlag.IS_INLINE) + } + if (Flag.PropertyAccessor.IS_NOT_DEFAULT(this@propertyAccessorFlags)) { + add(PropertyAccessorFlag.IS_NOT_DEFAULT) + } + } + +internal inline fun <E> setOf(body: MutableSet<E>.() -> Unit): Set<E> { + return mutableSetOf<E>().apply(body).toSet() +} diff --git a/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/KotlinPoetMetadata.kt b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/KotlinPoetMetadata.kt new file mode 100644 index 00000000..6cb3c91d --- /dev/null +++ b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/KotlinPoetMetadata.kt @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2019 Square, Inc. + * + * 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 + * + * https://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. + */ +@file:JvmName("KotlinPoetMetadata") +@file:Suppress("unused") + +package com.squareup.kotlinpoet.metadata + +import javax.lang.model.element.TypeElement +import kotlin.annotation.AnnotationTarget.CLASS +import kotlin.annotation.AnnotationTarget.FUNCTION +import kotlin.annotation.AnnotationTarget.PROPERTY +import kotlin.reflect.KClass +import kotlinx.metadata.KmClass +import kotlinx.metadata.jvm.KotlinClassHeader +import kotlinx.metadata.jvm.KotlinClassMetadata + +/** + * Indicates that a given API is part of the experimental KotlinPoet metadata support. This exists + * because kotlinx-metadata is not a stable API, and will remain in place until it is. + */ +@RequiresOptIn +@Retention(AnnotationRetention.BINARY) +@Target(CLASS, FUNCTION, PROPERTY) +public annotation class KotlinPoetMetadataPreview + +/** @return a new [KmClass] representation of the Kotlin metadata for [this] class. */ +@KotlinPoetMetadataPreview +public fun KClass<*>.toKmClass(): KmClass = java.toKmClass() + +/** @return a new [KmClass] representation of the Kotlin metadata for [this] class. */ +@KotlinPoetMetadataPreview +public fun Class<*>.toKmClass(): KmClass = readMetadata(::getAnnotation).toKmClass() + +/** @return a new [KmClass] representation of the Kotlin metadata for [this] type. */ +@KotlinPoetMetadataPreview +public fun TypeElement.toKmClass(): KmClass = readMetadata(::getAnnotation).toKmClass() + +@KotlinPoetMetadataPreview +public fun Metadata.toKmClass(): KmClass { + return toKotlinClassMetadata<KotlinClassMetadata.Class>() + .toKmClass() +} + +@KotlinPoetMetadataPreview +public inline fun <reified T : KotlinClassMetadata> Metadata.toKotlinClassMetadata(): T { + val expectedType = T::class + val metadata = readKotlinClassMetadata() + return when (expectedType) { + KotlinClassMetadata.Class::class -> { + check(metadata is KotlinClassMetadata.Class) + metadata as T + } + KotlinClassMetadata.FileFacade::class -> { + check(metadata is KotlinClassMetadata.FileFacade) + metadata as T + } + KotlinClassMetadata.SyntheticClass::class -> + throw UnsupportedOperationException("SyntheticClass isn't supported yet!") + KotlinClassMetadata.MultiFileClassFacade::class -> + throw UnsupportedOperationException("MultiFileClassFacade isn't supported yet!") + KotlinClassMetadata.MultiFileClassPart::class -> + throw UnsupportedOperationException("MultiFileClassPart isn't supported yet!") + KotlinClassMetadata.Unknown::class -> + throw RuntimeException("Recorded unknown metadata type! $metadata") + else -> TODO("Unrecognized KotlinClassMetadata type: $expectedType") + } +} + +/** + * Returns the [KotlinClassMetadata] this represents. In general you should only use this function + * when you don't know what the underlying [KotlinClassMetadata] subtype is, otherwise you should + * use one of the more direct functions like [toKmClass]. + */ +@KotlinPoetMetadataPreview +public fun Metadata.readKotlinClassMetadata(): KotlinClassMetadata { + val metadata = KotlinClassMetadata.read(asClassHeader()) + checkNotNull(metadata) { + "Could not parse metadata! Try bumping kotlinpoet and/or kotlinx-metadata version." + } + return metadata +} + +private inline fun readMetadata(lookup: ((Class<Metadata>) -> Metadata?)): Metadata { + return checkNotNull(lookup.invoke(Metadata::class.java)) { + "No Metadata annotation found! Must be Kotlin code built with the standard library on the classpath." + } +} + +private fun Metadata.asClassHeader(): KotlinClassHeader { + return KotlinClassHeader( + kind = kind, + metadataVersion = metadataVersion, + data1 = data1, + data2 = data2, + extraString = extraString, + packageName = packageName, + extraInt = extraInt, + ) +} diff --git a/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/classinspectors/ClassInspectorUtil.kt b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/classinspectors/ClassInspectorUtil.kt new file mode 100644 index 00000000..d9caa834 --- /dev/null +++ b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/classinspectors/ClassInspectorUtil.kt @@ -0,0 +1,241 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * 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 + * + * https://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.squareup.kotlinpoet.metadata.classinspectors + +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.AnnotationSpec.UseSiteTarget +import com.squareup.kotlinpoet.AnnotationSpec.UseSiteTarget.FIELD +import com.squareup.kotlinpoet.CHAR_SEQUENCE +import com.squareup.kotlinpoet.COLLECTION +import com.squareup.kotlinpoet.COMPARABLE +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.ITERABLE +import com.squareup.kotlinpoet.LIST +import com.squareup.kotlinpoet.MAP +import com.squareup.kotlinpoet.MAP_ENTRY +import com.squareup.kotlinpoet.MUTABLE_COLLECTION +import com.squareup.kotlinpoet.MUTABLE_ITERABLE +import com.squareup.kotlinpoet.MUTABLE_LIST +import com.squareup.kotlinpoet.MUTABLE_MAP +import com.squareup.kotlinpoet.MUTABLE_MAP_ENTRY +import com.squareup.kotlinpoet.MUTABLE_SET +import com.squareup.kotlinpoet.SET +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.asClassName +import com.squareup.kotlinpoet.joinToCode +import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview +import com.squareup.kotlinpoet.metadata.isConst +import com.squareup.kotlinpoet.metadata.specs.ClassInspector +import java.util.Collections +import java.util.TreeSet +import kotlinx.metadata.KmProperty +import kotlinx.metadata.isLocal +import org.jetbrains.annotations.NotNull +import org.jetbrains.annotations.Nullable + +@KotlinPoetMetadataPreview +internal object ClassInspectorUtil { + val JVM_NAME: ClassName = JvmName::class.asClassName() + private val JVM_FIELD = JvmField::class.asClassName() + internal val JVM_FIELD_SPEC = AnnotationSpec.builder(JVM_FIELD).build() + internal val JVM_SYNTHETIC = JvmSynthetic::class.asClassName() + internal val JVM_SYNTHETIC_SPEC = AnnotationSpec.builder(JVM_SYNTHETIC).build() + internal val JAVA_DEPRECATED = java.lang.Deprecated::class.asClassName() + private val JVM_TRANSIENT = Transient::class.asClassName() + private val JVM_VOLATILE = Volatile::class.asClassName() + private val IMPLICIT_FIELD_ANNOTATIONS = setOf( + JVM_FIELD, + JVM_TRANSIENT, + JVM_VOLATILE, + ) + private val NOT_NULL = NotNull::class.asClassName() + private val NULLABLE = Nullable::class.asClassName() + private val EXTENSION_FUNCTION_TYPE = ExtensionFunctionType::class.asClassName() + private val KOTLIN_INTRINSIC_ANNOTATIONS = setOf( + NOT_NULL, + NULLABLE, + EXTENSION_FUNCTION_TYPE, + ) + + val KOTLIN_INTRINSIC_INTERFACES: Set<ClassName> = setOf( + CHAR_SEQUENCE, + COMPARABLE, + ITERABLE, + COLLECTION, + LIST, + SET, + MAP, + MAP_ENTRY, + MUTABLE_ITERABLE, + MUTABLE_COLLECTION, + MUTABLE_LIST, + MUTABLE_SET, + MUTABLE_MAP, + MUTABLE_MAP_ENTRY, + ) + + private val KOTLIN_NULLABILITY_ANNOTATIONS = setOf( + "org.jetbrains.annotations.NotNull", + "org.jetbrains.annotations.Nullable", + ) + + fun filterOutNullabilityAnnotations( + annotations: List<AnnotationSpec>, + ): List<AnnotationSpec> { + return annotations.filterNot { + val typeName = it.typeName + return@filterNot typeName is ClassName && + typeName.canonicalName in KOTLIN_NULLABILITY_ANNOTATIONS + } + } + + /** @return a [CodeBlock] representation of a [literal] value. */ + fun codeLiteralOf(literal: Any): CodeBlock { + return when (literal) { + is String -> CodeBlock.of("%S", literal) + is Long -> CodeBlock.of("%LL", literal) + is Float -> CodeBlock.of("%LF", literal) + else -> CodeBlock.of("%L", literal) + } + } + + /** + * Infers if [property] is a jvm field and should be annotated as such given the input + * parameters. + */ + fun computeIsJvmField( + property: KmProperty, + classInspector: ClassInspector, + isCompanionObject: Boolean, + hasGetter: Boolean, + hasSetter: Boolean, + hasField: Boolean, + ): Boolean { + return if (!hasGetter && + !hasSetter && + hasField && + !property.isConst + ) { + !(classInspector.supportsNonRuntimeRetainedAnnotations && !isCompanionObject) + } else { + false + } + } + + /** + * @return a new collection of [AnnotationSpecs][AnnotationSpec] with sorting and de-duping + * input annotations from [body]. + */ + fun createAnnotations( + siteTarget: UseSiteTarget? = null, + body: MutableCollection<AnnotationSpec>.() -> Unit, + ): Collection<AnnotationSpec> { + val result = mutableSetOf<AnnotationSpec>() + .apply(body) + .filterNot { spec -> + spec.typeName in KOTLIN_INTRINSIC_ANNOTATIONS + } + val withUseSiteTarget = if (siteTarget != null) { + result.map { + if (!(siteTarget == FIELD && it.typeName in IMPLICIT_FIELD_ANNOTATIONS)) { + // Some annotations are implicitly only for FIELD, so don't emit those site targets + it.toBuilder().useSiteTarget(siteTarget).build() + } else { + it + } + } + } else { + result + } + + val sorted = withUseSiteTarget.toTreeSet() + + return Collections.unmodifiableCollection(sorted) + } + + /** + * @return a [@Throws][Throws] [AnnotationSpec] representation of a given collection of + * [exceptions]. + */ + fun createThrowsSpec( + exceptions: Collection<TypeName>, + useSiteTarget: UseSiteTarget? = null, + ): AnnotationSpec { + return AnnotationSpec.builder(Throws::class) + .addMember( + "exceptionClasses = %L", + exceptions.map { CodeBlock.of("%T::class", it) } + .joinToCode(prefix = "[", suffix = "]"), + ) + .useSiteTarget(useSiteTarget) + .build() + } + + /** + * Best guesses a [ClassName] as represented in Metadata's [kotlinx.metadata.ClassName], where + * package names in this name are separated by '/' and class names are separated by '.'. + * + * For example: `"org/foo/bar/Baz.Nested"`. + * + * Local classes are prefixed with ".", but for KotlinPoetMetadataSpecs' use case we don't deal + * with those. + */ + fun createClassName(kotlinMetadataName: String): ClassName { + require(!kotlinMetadataName.isLocal) { + "Local/anonymous classes are not supported!" + } + // Top-level: package/of/class/MyClass + // Nested A: package/of/class/MyClass.NestedClass + val simpleName = kotlinMetadataName.substringAfterLast( + '/', // Drop the package name, e.g. "package/of/class/" + '.', // Drop any enclosing classes, e.g. "MyClass." + ) + val packageName = kotlinMetadataName.substringBeforeLast( + delimiter = "/", + missingDelimiterValue = "", + ) + val simpleNames = kotlinMetadataName.removeSuffix(simpleName) + .removeSuffix(".") // Trailing "." if any + .removePrefix(packageName) + .removePrefix("/") + .let { + if (it.isNotEmpty()) { + it.split(".") + } else { + // Don't split, otherwise we end up with an empty string as the first element! + emptyList() + } + } + .plus(simpleName) + + return ClassName( + packageName = packageName.replace("/", "."), + simpleNames = simpleNames, + ) + } + + fun Iterable<AnnotationSpec>.toTreeSet(): TreeSet<AnnotationSpec> { + return TreeSet<AnnotationSpec>(compareBy { it.toString() }).apply { + addAll(this@toTreeSet) + } + } + + private fun String.substringAfterLast(vararg delimiters: Char): String { + val index = lastIndexOfAny(delimiters) + return if (index == -1) this else substring(index + 1, length) + } +} diff --git a/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/classinspectors/ElementsClassInspector.kt b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/classinspectors/ElementsClassInspector.kt new file mode 100644 index 00000000..d4f77860 --- /dev/null +++ b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/classinspectors/ElementsClassInspector.kt @@ -0,0 +1,585 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * 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 + * + * https://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.squareup.kotlinpoet.metadata.classinspectors + +import com.google.auto.common.MoreElements +import com.google.auto.common.MoreTypes +import com.google.auto.common.Visibility +import com.google.common.collect.LinkedHashMultimap +import com.google.common.collect.SetMultimap +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.AnnotationSpec.UseSiteTarget.FILE +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.asClassName +import com.squareup.kotlinpoet.asTypeName +import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview +import com.squareup.kotlinpoet.metadata.classinspectors.ClassInspectorUtil.JAVA_DEPRECATED +import com.squareup.kotlinpoet.metadata.classinspectors.ClassInspectorUtil.JVM_NAME +import com.squareup.kotlinpoet.metadata.classinspectors.ClassInspectorUtil.filterOutNullabilityAnnotations +import com.squareup.kotlinpoet.metadata.hasAnnotations +import com.squareup.kotlinpoet.metadata.hasConstant +import com.squareup.kotlinpoet.metadata.isAnnotation +import com.squareup.kotlinpoet.metadata.isCompanionObject +import com.squareup.kotlinpoet.metadata.isConst +import com.squareup.kotlinpoet.metadata.isDeclaration +import com.squareup.kotlinpoet.metadata.isSynthesized +import com.squareup.kotlinpoet.metadata.isValue +import com.squareup.kotlinpoet.metadata.readKotlinClassMetadata +import com.squareup.kotlinpoet.metadata.specs.ClassData +import com.squareup.kotlinpoet.metadata.specs.ClassInspector +import com.squareup.kotlinpoet.metadata.specs.ConstructorData +import com.squareup.kotlinpoet.metadata.specs.ContainerData +import com.squareup.kotlinpoet.metadata.specs.EnumEntryData +import com.squareup.kotlinpoet.metadata.specs.FieldData +import com.squareup.kotlinpoet.metadata.specs.FileData +import com.squareup.kotlinpoet.metadata.specs.JvmFieldModifier +import com.squareup.kotlinpoet.metadata.specs.JvmFieldModifier.TRANSIENT +import com.squareup.kotlinpoet.metadata.specs.JvmFieldModifier.VOLATILE +import com.squareup.kotlinpoet.metadata.specs.JvmMethodModifier +import com.squareup.kotlinpoet.metadata.specs.JvmMethodModifier.DEFAULT +import com.squareup.kotlinpoet.metadata.specs.JvmMethodModifier.STATIC +import com.squareup.kotlinpoet.metadata.specs.JvmMethodModifier.SYNCHRONIZED +import com.squareup.kotlinpoet.metadata.specs.KM_CONSTRUCTOR_COMPARATOR +import com.squareup.kotlinpoet.metadata.specs.KM_FUNCTION_COMPARATOR +import com.squareup.kotlinpoet.metadata.specs.KM_PROPERTY_COMPARATOR +import com.squareup.kotlinpoet.metadata.specs.MethodData +import com.squareup.kotlinpoet.metadata.specs.PropertyData +import com.squareup.kotlinpoet.metadata.toKmClass +import java.util.TreeMap +import java.util.concurrent.ConcurrentHashMap +import javax.lang.model.element.Element +import javax.lang.model.element.ElementKind.INTERFACE +import javax.lang.model.element.ExecutableElement +import javax.lang.model.element.PackageElement +import javax.lang.model.element.TypeElement +import javax.lang.model.element.VariableElement +import javax.lang.model.type.TypeKind +import javax.lang.model.util.ElementFilter +import javax.lang.model.util.Elements +import javax.lang.model.util.Types +import kotlin.LazyThreadSafetyMode.NONE +import kotlinx.metadata.KmClass +import kotlinx.metadata.KmDeclarationContainer +import kotlinx.metadata.KmPackage +import kotlinx.metadata.jvm.JvmFieldSignature +import kotlinx.metadata.jvm.JvmMethodSignature +import kotlinx.metadata.jvm.KotlinClassMetadata +import kotlinx.metadata.jvm.fieldSignature +import kotlinx.metadata.jvm.getterSignature +import kotlinx.metadata.jvm.setterSignature +import kotlinx.metadata.jvm.signature +import kotlinx.metadata.jvm.syntheticMethodForAnnotations + +private typealias ElementsModifier = javax.lang.model.element.Modifier + +/** + * An [Elements]-based implementation of [ClassInspector]. + */ +@KotlinPoetMetadataPreview +public class ElementsClassInspector private constructor( + private val elements: Elements, + private val types: Types, +) : ClassInspector { + private val typeElementCache = ConcurrentHashMap<ClassName, Optional<TypeElement>>() + private val methodCache = ConcurrentHashMap<Pair<TypeElement, String>, Optional<ExecutableElement>>() + private val variableElementCache = ConcurrentHashMap<Pair<TypeElement, String>, Optional<VariableElement>>() + private val jvmNameType = elements.getTypeElement(JVM_NAME.canonicalName) + private val jvmNameName = ElementFilter.methodsIn(jvmNameType.enclosedElements) + .first { it.simpleName.toString() == "name" } + + private fun lookupTypeElement(className: ClassName): TypeElement? { + return typeElementCache.getOrPut(className) { + elements.getTypeElement(className.canonicalName).toOptional() + }.nullableValue + } + + override val supportsNonRuntimeRetainedAnnotations: Boolean = true + + override fun declarationContainerFor(className: ClassName): KmDeclarationContainer { + val typeElement = lookupTypeElement(className) + ?: error("No type element found for: $className.") + + val metadata = typeElement.getAnnotation(Metadata::class.java) + return when (val kotlinClassMetadata = metadata.readKotlinClassMetadata()) { + is KotlinClassMetadata.Class -> kotlinClassMetadata.toKmClass() + is KotlinClassMetadata.FileFacade -> kotlinClassMetadata.toKmPackage() + else -> TODO("Not implemented yet: ${kotlinClassMetadata.javaClass.simpleName}") + } + } + + override fun isInterface(className: ClassName): Boolean { + if (className in ClassInspectorUtil.KOTLIN_INTRINSIC_INTERFACES) { + return true + } + return lookupTypeElement(className)?.kind == INTERFACE + } + + private fun TypeElement.lookupField(fieldSignature: JvmFieldSignature): VariableElement? { + val signatureString = fieldSignature.asString() + return variableElementCache.getOrPut(this to signatureString) { + ElementFilter.fieldsIn(enclosedElements) + .find { signatureString == it.jvmFieldSignature(types) }.toOptional() + }.nullableValue + } + + private fun lookupMethod( + className: ClassName, + methodSignature: JvmMethodSignature, + elementFilter: (Iterable<Element>) -> List<ExecutableElement>, + ): ExecutableElement? { + return lookupTypeElement(className)?.lookupMethod(methodSignature, elementFilter) + } + + private fun TypeElement.lookupMethod( + methodSignature: JvmMethodSignature, + elementFilter: (Iterable<Element>) -> List<ExecutableElement>, + ): ExecutableElement? { + val signatureString = methodSignature.asString() + return methodCache.getOrPut(this to signatureString) { + elementFilter(enclosedElements) + .find { signatureString == it.jvmMethodSignature(types) }.toOptional() + }.nullableValue + } + + private fun VariableElement.jvmModifiers(isJvmField: Boolean): Set<JvmFieldModifier> { + return modifiers.mapNotNullTo(mutableSetOf()) { + when { + it == ElementsModifier.TRANSIENT -> TRANSIENT + it == ElementsModifier.VOLATILE -> VOLATILE + !isJvmField && it == ElementsModifier.STATIC -> JvmFieldModifier.STATIC + else -> null + } + } + } + + private fun VariableElement.annotationSpecs(): List<AnnotationSpec> { + @Suppress("DEPRECATION") + return filterOutNullabilityAnnotations( + annotationMirrors.map { AnnotationSpec.get(it) }, + ) + } + + private fun ExecutableElement.jvmModifiers(): Set<JvmMethodModifier> { + return modifiers.mapNotNullTo(mutableSetOf()) { + when (it) { + ElementsModifier.SYNCHRONIZED -> SYNCHRONIZED + ElementsModifier.STATIC -> STATIC + ElementsModifier.DEFAULT -> DEFAULT + else -> null + } + } + } + + private fun ExecutableElement.annotationSpecs(): List<AnnotationSpec> { + @Suppress("DEPRECATION") + return filterOutNullabilityAnnotations( + annotationMirrors.map { AnnotationSpec.get(it) }, + ) + } + + private fun ExecutableElement.exceptionTypeNames(): List<TypeName> { + @Suppress("DEPRECATION") + return thrownTypes.map { it.asTypeName() } + } + + override fun enumEntry(enumClassName: ClassName, memberName: String): EnumEntryData { + val enumType = lookupTypeElement(enumClassName) + ?: error("No type element found for: $enumClassName.") + val enumTypeAsType = enumType.asType() + val member = typeElementCache.getOrPut(enumClassName.nestedClass(memberName)) { + ElementFilter.typesIn(enumType.enclosedElements) + .asSequence() + .filter { types.isSubtype(enumTypeAsType, it.superclass) } + .find { it.simpleName.contentEquals(memberName) }.toOptional() + }.nullableValue + val declarationContainer = member?.getAnnotation(Metadata::class.java)?.toKmClass() + + val entry = ElementFilter.fieldsIn(enumType.enclosedElements) + .find { it.simpleName.contentEquals(memberName) } + ?: error("Could not find the enum entry for: $enumClassName") + + return EnumEntryData( + declarationContainer = declarationContainer, + annotations = entry.annotationSpecs(), + ) + } + + private fun VariableElement.constantValue(): CodeBlock? { + return constantValue?.let(ClassInspectorUtil::codeLiteralOf) + } + + override fun methodExists(className: ClassName, methodSignature: JvmMethodSignature): Boolean { + return lookupMethod(className, methodSignature, ElementFilter::methodsIn) != null + } + + /** + * Detects whether [this] given method is overridden in [type]. + * + * Adapted and simplified from AutoCommon's private + * [MoreElements.getLocalAndInheritedMethods] methods implementations for detecting + * overrides. + */ + private fun ExecutableElement.isOverriddenIn(type: TypeElement): Boolean { + val methodMap = LinkedHashMultimap.create<String, ExecutableElement>() + type.getAllMethods(MoreElements.getPackage(type), methodMap) + // Find methods that are overridden using `Elements.overrides`. We reduce the performance + // impact by: + // (a) grouping methods by name, since a method cannot override another method with a + // different name. Since we know the target name, we just inspect the methods with + // that name. + // (b) making sure that methods in ancestor types precede those in descendant types, + // which means we only have to check a method against the ones that follow it in + // that order. Below, this means we just need to find the index of our target method + // and compare against only preceding ones. + val methodList = methodMap.asMap()[simpleName.toString()]?.toList() + ?: return false + val signature = jvmMethodSignature(types) + return methodList.asSequence() + .filter { it.jvmMethodSignature(types) == signature } + .take(1) + .any { elements.overrides(this, it, type) } + } + + /** + * Add to [methodsAccumulator] the instance methods from [this] that are visible to code in + * the package [pkg]. This means all the instance methods from [this] itself and all + * instance methods it inherits from its ancestors, except private methods and + * package-private methods in other packages. This method does not take overriding into + * account, so it will add both an ancestor method and a descendant method that overrides + * it. [methodsAccumulator] is a multimap from a method name to all of the methods with + * that name, including methods that override or overload one another. Within those + * methods, those in ancestor types always precede those in descendant types. + * + * Adapted from AutoCommon's private [MoreElements.getLocalAndInheritedMethods] methods' + * implementations, before overridden methods are stripped. + */ + private fun TypeElement.getAllMethods( + pkg: PackageElement, + methodsAccumulator: SetMultimap<String, ExecutableElement>, + ) { + for (superInterface in interfaces) { + MoreTypes.asTypeElement(superInterface).getAllMethods(pkg, methodsAccumulator) + } + if (superclass.kind != TypeKind.NONE) { + // Visit the superclass after superinterfaces so we will always see the implementation of a + // method after any interfaces that declared it. + MoreTypes.asTypeElement(superclass).getAllMethods(pkg, methodsAccumulator) + } + for (method in ElementFilter.methodsIn(enclosedElements)) { + if (ElementsModifier.STATIC !in method.modifiers && + ElementsModifier.FINAL !in method.modifiers && + ElementsModifier.PRIVATE !in method.modifiers && + method.isVisibleFrom(pkg) + ) { + methodsAccumulator.put(method.simpleName.toString(), method) + } + } + } + + private fun ExecutableElement.isVisibleFrom(pkg: PackageElement): Boolean { + // We use Visibility.ofElement rather than [MoreElements.effectiveVisibilityOfElement] + // because it doesn't really matter whether the containing class is visible. If you + // inherit a public method then you have a public method, regardless of whether you + // inherit it from a public class. + return when (Visibility.ofElement(this)) { + Visibility.PRIVATE -> false + Visibility.DEFAULT -> MoreElements.getPackage(this) == pkg + else -> true + } + } + + override fun containerData( + declarationContainer: KmDeclarationContainer, + className: ClassName, + parentClassName: ClassName?, + ): ContainerData { + val typeElement: TypeElement = lookupTypeElement(className) ?: error("No class found for: $className.") + val isCompanionObject = when (declarationContainer) { + is KmClass -> { + declarationContainer.isCompanionObject + } + is KmPackage -> { + false + } + else -> TODO("Not implemented yet: ${declarationContainer.javaClass.simpleName}") + } + + // Should only be called if parentName has been null-checked + val classIfCompanion by lazy(NONE) { + if (isCompanionObject && parentClassName != null) { + lookupTypeElement(parentClassName) + ?: error("No class found for: $parentClassName.") + } else { + typeElement + } + } + + val propertyData = declarationContainer.properties + .asSequence() + .filter { it.isDeclaration } + .filterNot { it.isSynthesized } + .associateWithTo(TreeMap(KM_PROPERTY_COMPARATOR)) { property -> + val isJvmField = ClassInspectorUtil.computeIsJvmField( + property = property, + classInspector = this, + isCompanionObject = isCompanionObject, + hasGetter = property.getterSignature != null, + hasSetter = property.setterSignature != null, + hasField = property.fieldSignature != null, + ) + + val fieldData = property.fieldSignature?.let fieldDataLet@{ fieldSignature -> + // Check the field in the parent first. For const/static/jvmField elements, these only + // exist in the parent and we want to check that if necessary to avoid looking up a + // non-existent field in the companion. + val parentModifiers = if (isCompanionObject && parentClassName != null) { + classIfCompanion.lookupField(fieldSignature)?.jvmModifiers(isJvmField).orEmpty() + } else { + emptySet() + } + + val isStatic = JvmFieldModifier.STATIC in parentModifiers + + // TODO we looked up field once, let's reuse it + val classForOriginalField = typeElement.takeUnless { + isCompanionObject && + (property.isConst || isJvmField || isStatic) + } ?: classIfCompanion + + val field = classForOriginalField.lookupField(fieldSignature) + ?: return@fieldDataLet FieldData.SYNTHETIC + val constant = if (property.hasConstant) { + val fieldWithConstant = classIfCompanion.takeIf { it != typeElement }?.let { + if (it.kind.isInterface) { + field + } else { + // const properties are relocated to the enclosing class + it.lookupField(fieldSignature) + ?: return@fieldDataLet FieldData.SYNTHETIC + } + } ?: field + fieldWithConstant.constantValue() + } else { + null + } + + val jvmModifiers = field.jvmModifiers(isJvmField) + parentModifiers + + FieldData( + annotations = field.annotationSpecs(), + isSynthetic = false, + jvmModifiers = jvmModifiers.filterNotTo(mutableSetOf()) { + // JvmField companion objects don't need JvmStatic, it's implicit + isCompanionObject && isJvmField && it == JvmFieldModifier.STATIC + }, + constant = constant, + ) + } + + val getterData = property.getterSignature?.let { getterSignature -> + val method = classIfCompanion.lookupMethod(getterSignature, ElementFilter::methodsIn) + method?.methodData( + typeElement = typeElement, + hasAnnotations = property.getterFlags.hasAnnotations, + jvmInformationMethod = classIfCompanion.takeIf { it != typeElement } + ?.lookupMethod(getterSignature, ElementFilter::methodsIn) + ?: method, + ) + ?: return@let MethodData.SYNTHETIC + } + + val setterData = property.setterSignature?.let { setterSignature -> + val method = classIfCompanion.lookupMethod(setterSignature, ElementFilter::methodsIn) + method?.methodData( + typeElement = typeElement, + hasAnnotations = property.setterFlags.hasAnnotations, + jvmInformationMethod = classIfCompanion.takeIf { it != typeElement } + ?.lookupMethod(setterSignature, ElementFilter::methodsIn) + ?: method, + knownIsOverride = getterData?.isOverride, + ) + ?: return@let MethodData.SYNTHETIC + } + + val annotations = mutableListOf<AnnotationSpec>() + if (property.flags.hasAnnotations) { + property.syntheticMethodForAnnotations?.let { annotationsHolderSignature -> + val method = typeElement.lookupMethod(annotationsHolderSignature, ElementFilter::methodsIn) + ?: return@let MethodData.SYNTHETIC + annotations += method.annotationSpecs() + // Cover for https://github.com/square/kotlinpoet/issues/1046 + .filterNot { it.typeName == JAVA_DEPRECATED } + } + } + + // If a field is static in a companion object, remove the modifier and add the annotation + // directly on the top level. Otherwise this will generate `@field:JvmStatic`, which is + // not legal + var finalFieldData = fieldData + fieldData?.jvmModifiers?.let { modifiers -> + if (isCompanionObject && JvmFieldModifier.STATIC in modifiers) { + finalFieldData = fieldData.copy( + jvmModifiers = fieldData.jvmModifiers + .filterNotTo(LinkedHashSet()) { it == JvmFieldModifier.STATIC }, + ) + annotations += AnnotationSpec.builder( + JVM_STATIC, + ).build() + } + } + + PropertyData( + annotations = annotations, + fieldData = finalFieldData, + getterData = getterData, + setterData = setterData, + isJvmField = isJvmField, + ) + } + + val methodData = declarationContainer.functions + .associateWithTo(TreeMap(KM_FUNCTION_COMPARATOR)) { kmFunction -> + val signature = kmFunction.signature + if (signature != null) { + val method = typeElement.lookupMethod(signature, ElementFilter::methodsIn) + method?.methodData( + typeElement = typeElement, + hasAnnotations = kmFunction.flags.hasAnnotations, + jvmInformationMethod = classIfCompanion.takeIf { it != typeElement } + ?.lookupMethod(signature, ElementFilter::methodsIn) + ?: method, + ) + ?: return@associateWithTo MethodData.SYNTHETIC + } else { + MethodData.EMPTY + } + } + + when (declarationContainer) { + is KmClass -> { + val constructorData = declarationContainer.constructors + .associateWithTo(TreeMap(KM_CONSTRUCTOR_COMPARATOR)) { kmConstructor -> + if (declarationContainer.isAnnotation || declarationContainer.isValue) { + // + // Annotations are interfaces in bytecode, but kotlin metadata will still report a + // constructor signature + // + // Inline classes have no constructors at runtime + // + return@associateWithTo ConstructorData.EMPTY + } + val signature = kmConstructor.signature + if (signature != null) { + val constructor = typeElement.lookupMethod(signature, ElementFilter::constructorsIn) + ?: return@associateWithTo ConstructorData.EMPTY + ConstructorData( + annotations = if (kmConstructor.flags.hasAnnotations) { + constructor.annotationSpecs() + } else { + emptyList() + }, + parameterAnnotations = constructor.parameters.indexedAnnotationSpecs(), + isSynthetic = false, + jvmModifiers = constructor.jvmModifiers(), + exceptions = constructor.exceptionTypeNames(), + ) + } else { + ConstructorData.EMPTY + } + } + return ClassData( + declarationContainer = declarationContainer, + className = className, + annotations = if (declarationContainer.flags.hasAnnotations) { + ClassInspectorUtil.createAnnotations { + @Suppress("DEPRECATION") + addAll(typeElement.annotationMirrors.map { AnnotationSpec.get(it) }) + } + } else { + emptyList() + }, + properties = propertyData, + constructors = constructorData, + methods = methodData, + ) + } + is KmPackage -> { + // There's no flag for checking if there are annotations, so we just eagerly check in this + // case. All annotations on this class are file: site targets in source. This includes + // @JvmName. + var jvmName: String? = null + val fileAnnotations = ClassInspectorUtil.createAnnotations(FILE) { + addAll( + typeElement.annotationMirrors.map { + if (it.annotationType == jvmNameType) { + val nameValue = requireNotNull(it.elementValues[jvmNameName]) { + "No name property found on $it" + } + jvmName = nameValue.value as String + } + @Suppress("DEPRECATION") + AnnotationSpec.get(it) + }, + ) + } + return FileData( + declarationContainer = declarationContainer, + annotations = fileAnnotations, + properties = propertyData, + methods = methodData, + className = className, + jvmName = jvmName, + ) + } + else -> TODO("Not implemented yet: ${declarationContainer.javaClass.simpleName}") + } + } + + private fun List<VariableElement>.indexedAnnotationSpecs(): Map<Int, Collection<AnnotationSpec>> { + return withIndex().associate { (index, parameter) -> + index to ClassInspectorUtil.createAnnotations { addAll(parameter.annotationSpecs()) } + } + } + + private fun ExecutableElement.methodData( + typeElement: TypeElement, + hasAnnotations: Boolean, + jvmInformationMethod: ExecutableElement = this, + knownIsOverride: Boolean? = null, + ): MethodData { + return MethodData( + annotations = if (hasAnnotations) annotationSpecs() else emptyList(), + parameterAnnotations = parameters.indexedAnnotationSpecs(), + isSynthetic = false, + jvmModifiers = jvmInformationMethod.jvmModifiers(), + isOverride = knownIsOverride ?: isOverriddenIn(typeElement), + exceptions = exceptionTypeNames(), + ) + } + + public companion object { + /** @return an [Elements]-based implementation of [ClassInspector]. */ + @JvmStatic + @KotlinPoetMetadataPreview + public fun create(elements: Elements, types: Types): ClassInspector { + return ElementsClassInspector(elements, types) + } + + private val JVM_STATIC = JvmStatic::class.asClassName() + } +} diff --git a/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/classinspectors/JvmDescriptorUtils.kt b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/classinspectors/JvmDescriptorUtils.kt new file mode 100644 index 00000000..235e5e87 --- /dev/null +++ b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/classinspectors/JvmDescriptorUtils.kt @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * 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 + * + * https://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.squareup.kotlinpoet.metadata.classinspectors + +import javax.lang.model.element.Element +import javax.lang.model.element.ExecutableElement +import javax.lang.model.element.NestingKind +import javax.lang.model.element.QualifiedNameable +import javax.lang.model.element.TypeElement +import javax.lang.model.element.VariableElement +import javax.lang.model.type.ArrayType +import javax.lang.model.type.DeclaredType +import javax.lang.model.type.ErrorType +import javax.lang.model.type.ExecutableType +import javax.lang.model.type.NoType +import javax.lang.model.type.NullType +import javax.lang.model.type.PrimitiveType +import javax.lang.model.type.TypeKind.BOOLEAN +import javax.lang.model.type.TypeKind.BYTE +import javax.lang.model.type.TypeKind.CHAR +import javax.lang.model.type.TypeKind.DOUBLE +import javax.lang.model.type.TypeKind.FLOAT +import javax.lang.model.type.TypeKind.INT +import javax.lang.model.type.TypeKind.LONG +import javax.lang.model.type.TypeKind.SHORT +import javax.lang.model.type.TypeMirror +import javax.lang.model.type.TypeVariable +import javax.lang.model.type.WildcardType +import javax.lang.model.util.AbstractTypeVisitor6 +import javax.lang.model.util.Types +import kotlinx.metadata.jvm.JvmFieldSignature +import kotlinx.metadata.jvm.JvmMethodSignature + +/* + * Adapted from + * - https://github.com/Takhion/kotlin-metadata/blob/e6de126575ad6ca10b093129b7c30d000c9b0c37/lib/src/main/kotlin/me/eugeniomarletti/kotlin/metadata/jvm/JvmDescriptorUtils.kt + * - https://github.com/Takhion/kotlin-metadata/pull/13 + */ + +/** + * For reference, see the [JVM specification, section 4.2](https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.2). + * + * @return the name of this [Element] in its "internal form". + */ +internal val Element.internalName: String + get() = when (this) { + is TypeElement -> { + when (nestingKind) { + NestingKind.TOP_LEVEL -> + qualifiedName.toString().replace('.', '/') + NestingKind.MEMBER -> + enclosingElement.internalName + "$" + simpleName + NestingKind.LOCAL, NestingKind.ANONYMOUS -> + error("Unsupported nesting $nestingKind") + null -> + error("Unsupported, nestingKind == null") + } + } + is QualifiedNameable -> qualifiedName.toString().replace('.', '/') + else -> simpleName.toString() + } + +/** + * @return the "field descriptor" of this type. + * @see [JvmDescriptorTypeVisitor] + */ +@Suppress("unused") +internal val NoType.descriptor: String + get() = "V" + +/** + * @return the "field descriptor" of this type. + * @see [JvmDescriptorTypeVisitor] + */ +internal val DeclaredType.descriptor: String + get() = "L" + asElement().internalName + ";" + +/** + * @return the "field descriptor" of this type. + * @see [JvmDescriptorTypeVisitor] + */ +internal val PrimitiveType.descriptor: String + get() = when (this.kind) { + BYTE -> "B" + CHAR -> "C" + DOUBLE -> "D" + FLOAT -> "F" + INT -> "I" + LONG -> "J" + SHORT -> "S" + BOOLEAN -> "Z" + else -> error("Unknown primitive type $this") + } + +/** + * @see [JvmDescriptorTypeVisitor] + */ +internal fun TypeMirror.descriptor(types: Types): String = + accept(JvmDescriptorTypeVisitor, types) + +/** + * @return the "field descriptor" of this type. + * @see [JvmDescriptorTypeVisitor] + */ +internal fun WildcardType.descriptor(types: Types): String = + types.erasure(this).descriptor(types) + +/** + * @return the "field descriptor" of this type. + * @see [JvmDescriptorTypeVisitor] + */ +internal fun TypeVariable.descriptor(types: Types): String = + types.erasure(this).descriptor(types) + +/** + * @return the "field descriptor" of this type. + * @see [JvmDescriptorTypeVisitor] + */ +internal fun ArrayType.descriptor(types: Types): String = + "[" + componentType.descriptor(types) + +/** + * @return the "method descriptor" of this type. + * @see [JvmDescriptorTypeVisitor] + */ +internal fun ExecutableType.descriptor(types: Types): String { + val parameterDescriptors = parameterTypes.joinToString(separator = "") { it.descriptor(types) } + val returnDescriptor = returnType.descriptor(types) + return "($parameterDescriptors)$returnDescriptor" +} + +/** + * Returns the JVM signature in the form "$Name$MethodDescriptor", for example: `equals(Ljava/lang/Object;)Z`. + * + * Useful for comparing with [JvmMethodSignature]. + * + * For reference, see the [JVM specification, section 4.3](https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3). + */ +internal fun ExecutableElement.jvmMethodSignature(types: Types): String { + return "$simpleName${asType().descriptor(types)}" +} + +/** + * Returns the JVM signature in the form "$Name:$FieldDescriptor", for example: `"value:Ljava/lang/String;"`. + * + * Useful for comparing with [JvmFieldSignature]. + * + * For reference, see the [JVM specification, section 4.3](https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3). + */ +internal fun VariableElement.jvmFieldSignature(types: Types): String { + return "$simpleName:${asType().descriptor(types)}" +} + +/** + * When applied over a type, it returns either: + * - a "field descriptor", for example: `Ljava/lang/Object;` + * - a "method descriptor", for example: `(Ljava/lang/Object;)Z` + * + * For reference, see the [JVM specification, section 4.3](https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3). + */ +internal object JvmDescriptorTypeVisitor : AbstractTypeVisitor6<String, Types>() { + override fun visitNoType(t: NoType, types: Types): String = t.descriptor + override fun visitDeclared(t: DeclaredType, types: Types): String = t.descriptor + override fun visitPrimitive(t: PrimitiveType, types: Types): String = t.descriptor + + override fun visitArray(t: ArrayType, types: Types): String = t.descriptor(types) + override fun visitWildcard(t: WildcardType, types: Types): String = t.descriptor(types) + override fun visitExecutable(t: ExecutableType, types: Types): String = t.descriptor(types) + override fun visitTypeVariable(t: TypeVariable, types: Types): String = t.descriptor(types) + + override fun visitNull(t: NullType, types: Types): String = visitUnknown( + t, + types, + ) + override fun visitError(t: ErrorType, types: Types): String = visitUnknown( + t, + types, + ) + + override fun visitUnknown(t: TypeMirror, types: Types): String = error("Unsupported type $t") +} diff --git a/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/classinspectors/Optional.kt b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/classinspectors/Optional.kt new file mode 100644 index 00000000..41935e36 --- /dev/null +++ b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/classinspectors/Optional.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * 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 + * + * https://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.squareup.kotlinpoet.metadata.classinspectors + +/** + * Simple `Optional` implementation for use in collections that don't allow `null` values. + */ +internal data class Optional<out T : Any>(val nullableValue: T?) + +internal fun <T : Any> T?.toOptional(): Optional<T> = Optional( + this, +) diff --git a/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/classinspectors/ReflectiveClassInspector.kt b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/classinspectors/ReflectiveClassInspector.kt new file mode 100644 index 00000000..fc31ba09 --- /dev/null +++ b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/classinspectors/ReflectiveClassInspector.kt @@ -0,0 +1,604 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * 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 + * + * https://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.squareup.kotlinpoet.metadata.classinspectors + +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.AnnotationSpec.UseSiteTarget.FILE +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.asTypeName +import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview +import com.squareup.kotlinpoet.metadata.classinspectors.ClassInspectorUtil.JAVA_DEPRECATED +import com.squareup.kotlinpoet.metadata.classinspectors.ClassInspectorUtil.filterOutNullabilityAnnotations +import com.squareup.kotlinpoet.metadata.hasAnnotations +import com.squareup.kotlinpoet.metadata.hasConstant +import com.squareup.kotlinpoet.metadata.isAnnotation +import com.squareup.kotlinpoet.metadata.isCompanionObject +import com.squareup.kotlinpoet.metadata.isConst +import com.squareup.kotlinpoet.metadata.isDeclaration +import com.squareup.kotlinpoet.metadata.isSynthesized +import com.squareup.kotlinpoet.metadata.isValue +import com.squareup.kotlinpoet.metadata.readKotlinClassMetadata +import com.squareup.kotlinpoet.metadata.specs.ClassData +import com.squareup.kotlinpoet.metadata.specs.ClassInspector +import com.squareup.kotlinpoet.metadata.specs.ConstructorData +import com.squareup.kotlinpoet.metadata.specs.ContainerData +import com.squareup.kotlinpoet.metadata.specs.EnumEntryData +import com.squareup.kotlinpoet.metadata.specs.FieldData +import com.squareup.kotlinpoet.metadata.specs.FileData +import com.squareup.kotlinpoet.metadata.specs.JvmFieldModifier +import com.squareup.kotlinpoet.metadata.specs.JvmFieldModifier.TRANSIENT +import com.squareup.kotlinpoet.metadata.specs.JvmFieldModifier.VOLATILE +import com.squareup.kotlinpoet.metadata.specs.JvmMethodModifier +import com.squareup.kotlinpoet.metadata.specs.JvmMethodModifier.DEFAULT +import com.squareup.kotlinpoet.metadata.specs.JvmMethodModifier.STATIC +import com.squareup.kotlinpoet.metadata.specs.JvmMethodModifier.SYNCHRONIZED +import com.squareup.kotlinpoet.metadata.specs.KM_CONSTRUCTOR_COMPARATOR +import com.squareup.kotlinpoet.metadata.specs.KM_FUNCTION_COMPARATOR +import com.squareup.kotlinpoet.metadata.specs.KM_PROPERTY_COMPARATOR +import com.squareup.kotlinpoet.metadata.specs.MethodData +import com.squareup.kotlinpoet.metadata.specs.PropertyData +import com.squareup.kotlinpoet.metadata.toKmClass +import java.lang.reflect.Constructor +import java.lang.reflect.Field +import java.lang.reflect.Method +import java.lang.reflect.Modifier +import java.lang.reflect.Parameter +import java.util.TreeMap +import java.util.concurrent.ConcurrentHashMap +import kotlin.LazyThreadSafetyMode.NONE +import kotlinx.metadata.KmClass +import kotlinx.metadata.KmDeclarationContainer +import kotlinx.metadata.KmPackage +import kotlinx.metadata.jvm.JvmFieldSignature +import kotlinx.metadata.jvm.JvmMethodSignature +import kotlinx.metadata.jvm.KotlinClassMetadata +import kotlinx.metadata.jvm.fieldSignature +import kotlinx.metadata.jvm.getterSignature +import kotlinx.metadata.jvm.setterSignature +import kotlinx.metadata.jvm.signature +import kotlinx.metadata.jvm.syntheticMethodForAnnotations + +@KotlinPoetMetadataPreview +public class ReflectiveClassInspector private constructor( + private val classLoader: ClassLoader?, +) : ClassInspector { + + private val classCache = ConcurrentHashMap<ClassName, Optional<Class<*>>>() + private val methodCache = ConcurrentHashMap<Pair<Class<*>, String>, Optional<Method>>() + private val constructorCache = ConcurrentHashMap<Pair<Class<*>, String>, Optional<Constructor<*>>>() + private val fieldCache = ConcurrentHashMap<Pair<Class<*>, String>, Optional<Field>>() + private val enumCache = ConcurrentHashMap<Pair<Class<*>, String>, Optional<Enum<*>>>() + + private fun lookupClass(className: ClassName): Class<*>? { + return classCache.getOrPut(className) { + try { + if (classLoader == null) { + Class.forName(className.reflectionName()) + } else { + Class.forName(className.reflectionName(), true, classLoader) + } + } catch (e: ClassNotFoundException) { + null + }.toOptional() + }.nullableValue + } + + override val supportsNonRuntimeRetainedAnnotations: Boolean = false + + override fun declarationContainerFor(className: ClassName): KmDeclarationContainer { + val clazz = lookupClass(className) + ?: error("No type element found for: $className.") + + val metadata = clazz.getAnnotation(Metadata::class.java) + return when (val kotlinClassMetadata = metadata.readKotlinClassMetadata()) { + is KotlinClassMetadata.Class -> kotlinClassMetadata.toKmClass() + is KotlinClassMetadata.FileFacade -> kotlinClassMetadata.toKmPackage() + else -> TODO("Not implemented yet: ${kotlinClassMetadata.javaClass.simpleName}") + } + } + + override fun isInterface(className: ClassName): Boolean { + if (className in ClassInspectorUtil.KOTLIN_INTRINSIC_INTERFACES) { + return true + } + return lookupClass(className)?.isInterface ?: false + } + + private fun Class<*>.lookupField(fieldSignature: JvmFieldSignature): Field? { + return try { + val signatureString = fieldSignature.asString() + fieldCache.getOrPut(this to signatureString) { + declaredFields + .asSequence() + .onEach { it.isAccessible = true } + .find { signatureString == it.jvmFieldSignature }.toOptional() + }.nullableValue + } catch (e: ClassNotFoundException) { + null + } + } + + private fun Class<*>.lookupMethod( + methodSignature: JvmMethodSignature, + ): Method? { + val signatureString = methodSignature.asString() + return methodCache.getOrPut(this to signatureString) { + declaredMethods + .asSequence() + .onEach { it.isAccessible = true } + .find { signatureString == it.jvmMethodSignature }.toOptional() + }.nullableValue + } + + private fun Class<*>.lookupConstructor( + constructorSignature: JvmMethodSignature, + ): Constructor<*>? { + val signatureString = constructorSignature.asString() + return constructorCache.getOrPut(this to signatureString) { + declaredConstructors + .asSequence() + .onEach { it.isAccessible = true } + .find { signatureString == it.jvmMethodSignature }.toOptional() + }.nullableValue + } + + private fun Field.jvmModifiers(): Set<JvmFieldModifier> { + return mutableSetOf<JvmFieldModifier>().apply { + if (Modifier.isTransient(modifiers)) { + add(TRANSIENT) + } + if (Modifier.isVolatile(modifiers)) { + add(VOLATILE) + } + if (Modifier.isStatic(modifiers)) { + add(JvmFieldModifier.STATIC) + } + } + } + + private fun Field.annotationSpecs(): List<AnnotationSpec> { + return filterOutNullabilityAnnotations( + declaredAnnotations.orEmpty().map { AnnotationSpec.get(it, includeDefaultValues = true) }, + ) + } + + private fun Constructor<*>.annotationSpecs(): List<AnnotationSpec> { + return filterOutNullabilityAnnotations( + declaredAnnotations.orEmpty().map { AnnotationSpec.get(it, true) }, + ) + } + + private fun Method.jvmModifiers(): Set<JvmMethodModifier> { + return methodJvmModifiers(modifiers, isDefault) + } + + private fun Constructor<*>.jvmModifiers(): Set<JvmMethodModifier> { + return methodJvmModifiers(modifiers, false) + } + + private fun methodJvmModifiers(modifiers: Int, isDefault: Boolean): Set<JvmMethodModifier> { + val jvmMethodModifiers = mutableSetOf<JvmMethodModifier>() + if (Modifier.isSynchronized(modifiers)) { + jvmMethodModifiers += SYNCHRONIZED + } + if (Modifier.isStatic(modifiers)) { + jvmMethodModifiers += STATIC + } + if (isDefault) { + jvmMethodModifiers += DEFAULT + } + return jvmMethodModifiers + } + + private fun Method.annotationSpecs(): List<AnnotationSpec> { + return filterOutNullabilityAnnotations( + declaredAnnotations.orEmpty().map { AnnotationSpec.get(it, includeDefaultValues = true) }, + ) + } + + private fun Parameter.annotationSpecs(): List<AnnotationSpec> { + return filterOutNullabilityAnnotations( + declaredAnnotations.map { AnnotationSpec.get(it, includeDefaultValues = true) }, + ) + } + + private fun Method.exceptionTypeNames(): List<TypeName> { + return exceptionTypes.orEmpty().mapTo(mutableListOf()) { it.asTypeName() } + } + + private fun Constructor<*>.exceptionTypeNames(): List<TypeName> { + return exceptionTypes.orEmpty().mapTo(mutableListOf()) { it.asTypeName() } + } + + override fun enumEntry(enumClassName: ClassName, memberName: String): EnumEntryData { + val clazz = lookupClass(enumClassName) + ?: error("No class found for: $enumClassName.") + check(clazz.isEnum) { + "Class must be an enum but isn't: $clazz" + } + val enumEntry = enumCache.getOrPut(clazz to memberName) { + clazz.enumConstants + .asSequence() + .map { it as Enum<*> } + .find { it.name == memberName } + .toOptional() + }.nullableValue + checkNotNull(enumEntry) { + "Could not find $memberName on $enumClassName" + } + return EnumEntryData( + declarationContainer = if (enumEntry.javaClass == clazz) { + // For simple enums with no class bodies, the entry class will be the same as the original + // class. + null + } else { + enumEntry.javaClass.getAnnotation(Metadata::class.java)?.toKmClass() + }, + annotations = clazz.getField(enumEntry.name).annotationSpecs(), + ) + } + + private fun Field.constantValue(): CodeBlock? { + if (!Modifier.isStatic(modifiers)) { + return null + } + return get(null) // Constant means we can do a static get on it. + .let(ClassInspectorUtil::codeLiteralOf) + } + + private fun JvmMethodSignature.isOverriddenIn(clazz: Class<*>): Boolean { + val signatureString = asString() + val classPackage = clazz.`package`.name + val interfaceMethods = clazz.interfaces.asSequence() + .flatMap { it.methods.asSequence() } + val superClassMethods = clazz.superclass?.methods.orEmpty().asSequence() + return interfaceMethods.plus(superClassMethods) + .filterNot { Modifier.isFinal(it.modifiers) } + .filterNot { Modifier.isStatic(it.modifiers) } + .filterNot { Modifier.isPrivate(it.modifiers) } + .filter { + Modifier.isPublic(it.modifiers) || + Modifier.isProtected(it.modifiers) || + // Package private + it.declaringClass.`package`.name == classPackage + } + .map { it.jvmMethodSignature } + .any { it == signatureString } + } + + override fun methodExists(className: ClassName, methodSignature: JvmMethodSignature): Boolean { + return lookupClass(className)?.lookupMethod(methodSignature) != null + } + + override fun containerData( + declarationContainer: KmDeclarationContainer, + className: ClassName, + parentClassName: ClassName?, + ): ContainerData { + val targetClass = lookupClass(className) ?: error("No class found for: $className.") + val isCompanionObject: Boolean = when (declarationContainer) { + is KmClass -> { + declarationContainer.isCompanionObject + } + is KmPackage -> { + false + } + else -> TODO("Not implemented yet: ${declarationContainer.javaClass.simpleName}") + } + + // Should only be called if parentName has been null-checked + val classIfCompanion by lazy(NONE) { + if (isCompanionObject && parentClassName != null) { + lookupClass(parentClassName) + ?: error("No class found for: $parentClassName.") + } else { + targetClass + } + } + + val propertyData = declarationContainer.properties + .asSequence() + .filter { it.isDeclaration } + .filterNot { it.isSynthesized } + .associateWithTo(TreeMap(KM_PROPERTY_COMPARATOR)) { property -> + val isJvmField = ClassInspectorUtil.computeIsJvmField( + property = property, + classInspector = this, + isCompanionObject = isCompanionObject, + hasGetter = property.getterSignature != null, + hasSetter = property.setterSignature != null, + hasField = property.fieldSignature != null, + ) + + val fieldData = property.fieldSignature?.let { fieldSignature -> + // Check the field in the parent first. For const/static/jvmField elements, these only + // exist in the parent and we want to check that if necessary to avoid looking up a + // non-existent field in the companion. + val parentModifiers = if (isCompanionObject && parentClassName != null) { + classIfCompanion.lookupField(fieldSignature)?.jvmModifiers().orEmpty() + } else { + emptySet() + } + + val isStatic = JvmFieldModifier.STATIC in parentModifiers + + // TODO we looked up field once, let's reuse it + val classForOriginalField = targetClass.takeUnless { + isCompanionObject && + (property.isConst || isJvmField || isStatic) + } ?: classIfCompanion + + val field = classForOriginalField.lookupField(fieldSignature) + ?: error("No field $fieldSignature found in $classForOriginalField.") + val constant = if (property.hasConstant) { + val fieldWithConstant = classIfCompanion.takeIf { it != targetClass }?.let { + if (it.isInterface) { + field + } else { + // const properties are relocated to the enclosing class + it.lookupField(fieldSignature) + ?: error("No field $fieldSignature found in $it.") + } + } ?: field + fieldWithConstant.constantValue() + } else { + null + } + + val jvmModifiers = field.jvmModifiers() + parentModifiers + + // For static, const, or JvmField fields in a companion object, the companion + // object's field is marked as synthetic to hide it from Java, but in this case + // it's a false positive for this check in kotlin. + val isSynthetic = field.isSynthetic && + !( + isCompanionObject && + (property.isConst || isJvmField || JvmFieldModifier.STATIC in jvmModifiers) + ) + + FieldData( + annotations = field.annotationSpecs(), + isSynthetic = isSynthetic, + jvmModifiers = jvmModifiers.filterNotTo(mutableSetOf()) { + // JvmField companion objects don't need JvmStatic, it's implicit + isCompanionObject && isJvmField && it == JvmFieldModifier.STATIC + }, + constant = constant, + ) + } + + val getterData = property.getterSignature?.let { getterSignature -> + val method = classIfCompanion.lookupMethod(getterSignature) + method?.methodData( + clazz = targetClass, + signature = getterSignature, + hasAnnotations = property.getterFlags.hasAnnotations, + jvmInformationMethod = classIfCompanion.takeIf { it != targetClass } + ?.lookupMethod(getterSignature) ?: method, + ) + ?: error("No getter method $getterSignature found in $classIfCompanion.") + } + + val setterData = property.setterSignature?.let { setterSignature -> + val method = classIfCompanion.lookupMethod(setterSignature) + method?.methodData( + clazz = targetClass, + signature = setterSignature, + hasAnnotations = property.setterFlags.hasAnnotations, + jvmInformationMethod = classIfCompanion.takeIf { it != targetClass } + ?.lookupMethod(setterSignature) ?: method, + knownIsOverride = getterData?.isOverride, + ) + ?: error("No setter method $setterSignature found in $classIfCompanion.") + } + + val annotations = mutableListOf<AnnotationSpec>() + if (property.flags.hasAnnotations) { + property.syntheticMethodForAnnotations?.let { annotationsHolderSignature -> + targetClass.lookupMethod(annotationsHolderSignature)?.let { method -> + annotations += method.annotationSpecs() + // Cover for https://github.com/square/kotlinpoet/issues/1046 + .filterNot { it.typeName == JAVA_DEPRECATED } + } + } + } + + PropertyData( + annotations = annotations, + fieldData = fieldData, + getterData = getterData, + setterData = setterData, + isJvmField = isJvmField, + ) + } + + val methodData = declarationContainer.functions + .associateWithTo(TreeMap(KM_FUNCTION_COMPARATOR)) { kmFunction -> + val signature = kmFunction.signature + if (signature != null) { + val method = targetClass.lookupMethod(signature) + method?.methodData( + clazz = targetClass, + signature = signature, + hasAnnotations = kmFunction.flags.hasAnnotations, + jvmInformationMethod = classIfCompanion.takeIf { it != targetClass }?.lookupMethod(signature) + ?: method, + ) + ?: error("No method $signature found in $targetClass.") + } else { + MethodData.EMPTY + } + } + + when (declarationContainer) { + is KmClass -> { + val classAnnotations = if (declarationContainer.flags.hasAnnotations) { + ClassInspectorUtil.createAnnotations { + addAll(targetClass.annotations.map { AnnotationSpec.get(it, includeDefaultValues = true) }) + } + } else { + emptyList() + } + val constructorData = declarationContainer.constructors + .associateWithTo(TreeMap(KM_CONSTRUCTOR_COMPARATOR)) { kmConstructor -> + if (declarationContainer.isAnnotation || declarationContainer.isValue) { + // + // Annotations are interfaces in reflection, but kotlin metadata will still report a + // constructor signature + // + // Inline classes have no constructors at runtime + // + return@associateWithTo ConstructorData.EMPTY + } + val signature = kmConstructor.signature + if (signature != null) { + val constructor = targetClass.lookupConstructor(signature) + ?: error("No constructor $signature found in $targetClass.") + ConstructorData( + annotations = if (kmConstructor.flags.hasAnnotations) { + constructor.annotationSpecs() + } else { + emptyList() + }, + parameterAnnotations = constructor.parameters.indexedAnnotationSpecs(), + isSynthetic = constructor.isSynthetic, + jvmModifiers = constructor.jvmModifiers(), + exceptions = constructor.exceptionTypeNames(), + ) + } else { + ConstructorData.EMPTY + } + } + return ClassData( + declarationContainer = declarationContainer, + className = className, + annotations = classAnnotations, + properties = propertyData, + constructors = constructorData, + methods = methodData, + ) + } + is KmPackage -> { + // There's no flag for checking if there are annotations, so we just eagerly check in this + // case. All annotations on this class are file: site targets in source. This does not + // include @JvmName since it does not have RUNTIME retention. In practice this doesn't + // really matter, but it does mean we can't know for certain if the file should be called + // FooKt.kt or Foo.kt. + val fileAnnotations = ClassInspectorUtil.createAnnotations(FILE) { + addAll(targetClass.annotations.map { AnnotationSpec.get(it, includeDefaultValues = true) }) + } + return FileData( + declarationContainer = declarationContainer, + annotations = fileAnnotations, + properties = propertyData, + methods = methodData, + className = className, + ) + } + else -> TODO("Not implemented yet: ${declarationContainer.javaClass.simpleName}") + } + } + + private fun Array<Parameter>.indexedAnnotationSpecs(): Map<Int, Collection<AnnotationSpec>> { + return withIndex().associate { (index, parameter) -> + index to ClassInspectorUtil.createAnnotations { addAll(parameter.annotationSpecs()) } + } + } + + private fun Method.methodData( + clazz: Class<*>, + signature: JvmMethodSignature, + hasAnnotations: Boolean, + jvmInformationMethod: Method = this, + knownIsOverride: Boolean? = null, + ): MethodData { + return MethodData( + annotations = if (hasAnnotations) annotationSpecs() else emptyList(), + parameterAnnotations = parameters.indexedAnnotationSpecs(), + isSynthetic = isSynthetic, + jvmModifiers = jvmInformationMethod.jvmModifiers(), + isOverride = knownIsOverride ?: signature.isOverriddenIn(clazz), + exceptions = exceptionTypeNames(), + ) + } + + public companion object { + @JvmStatic + @KotlinPoetMetadataPreview + public fun create(classLoader: ClassLoader? = null): ClassInspector { + return ReflectiveClassInspector(classLoader) + } + + private val Class<*>.descriptor: String + get() { + return when { + isPrimitive -> when (kotlin) { + Byte::class -> "B" + Char::class -> "C" + Double::class -> "D" + Float::class -> "F" + Int::class -> "I" + Long::class -> "J" + Short::class -> "S" + Boolean::class -> "Z" + Void::class -> "V" + else -> throw RuntimeException("Unrecognized primitive $this") + } + isArray -> name.replace('.', '/') + else -> "L$name;".replace('.', '/') + } + } + + private val Method.descriptor: String + get() = parameterTypes.joinToString( + separator = "", + prefix = "(", + postfix = ")${returnType.descriptor}", + ) { it.descriptor } + + /** + * Returns the JVM signature in the form "$Name$MethodDescriptor", for example: `equals(Ljava/lang/Object;)Z`. + * + * Useful for comparing with [JvmMethodSignature]. + * + * For reference, see the [JVM specification, section 4.3](https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3). + */ + private val Method.jvmMethodSignature: String get() = "$name$descriptor" + + private val Constructor<*>.descriptor: String + get() = parameterTypes.joinToString(separator = "", prefix = "(", postfix = ")V") { it.descriptor } + + /** + * Returns the JVM signature in the form "<init>$MethodDescriptor", for example: `"<init>(Ljava/lang/Object;)V")`. + * + * Useful for comparing with [JvmMethodSignature]. + * + * For reference, see the [JVM specification, section 4.3](https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3). + */ + private val Constructor<*>.jvmMethodSignature: String get() = "<init>$descriptor" + + /** + * Returns the JVM signature in the form "$Name:$FieldDescriptor", for example: `"value:Ljava/lang/String;"`. + * + * Useful for comparing with [JvmFieldSignature]. + * + * For reference, see the [JVM specification, section 4.3](https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3). + */ + private val Field.jvmFieldSignature: String get() = "$name:${type.descriptor}" + } +} diff --git a/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/ClassInspector.kt b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/ClassInspector.kt new file mode 100644 index 00000000..14c019fd --- /dev/null +++ b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/ClassInspector.kt @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2019 Square, Inc. + * + * 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 + * + * https://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.squareup.kotlinpoet.metadata.specs + +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview +import kotlinx.metadata.KmClass +import kotlinx.metadata.KmDeclarationContainer +import kotlinx.metadata.jvm.JvmMethodSignature + +/** A basic interface for looking up JVM information about a given Class. */ +@KotlinPoetMetadataPreview +public interface ClassInspector { + + /** + * Indicates if this [ClassInspector] supports [AnnotationRetention.RUNTIME]-retained annotations. + * This is used to indicate if manual inference of certain non-RUNTIME-retained annotations should + * be done, such as [JvmName]. + */ + public val supportsNonRuntimeRetainedAnnotations: Boolean + + /** + * Creates a new [ContainerData] instance for a given [declarationContainer]. + * + * @param declarationContainer the source [KmDeclarationContainer] to read from. + * @param className the [ClassName] of the target class to to read from. + * @param parentClassName the parent [ClassName] name if [declarationContainer] is nested, inner, + * or is a companion object. + */ + public fun containerData( + declarationContainer: KmDeclarationContainer, + className: ClassName, + parentClassName: ClassName?, + ): ContainerData + + /** + * Looks up other declaration containers, such as for nested members. Note that this class would + * always be Kotlin, so Metadata can be relied on for this. + * + * @param className The [ClassName] representation of the class. + * @return the read [KmDeclarationContainer] from its metadata. If no class or facade + * file was found, this should throw an exception. + */ + public fun declarationContainerFor(className: ClassName): KmDeclarationContainer + + /** + * Looks up a class and returns whether or not it is an interface. Note that this class can be + * Java or Kotlin, so Metadata should not be relied on for this. + * + * @param className The [ClassName] representation of the class. + * @return whether or not it is an interface. + */ + public fun isInterface(className: ClassName): Boolean + + /** + * Looks up the enum entry on a given enum given its member name. + * + * @param enumClassName The [ClassName] representation of the enum class. + * @param memberName The simple member name. + * @return the [EnumEntryData] + */ + public fun enumEntry(enumClassName: ClassName, memberName: String): EnumEntryData + + /** + * Looks up if a given [methodSignature] within [className] exists. + * + * @param className The [ClassName] representation of the class. + * @param methodSignature The method signature to check. + * @return whether or not the method exists. + */ + public fun methodExists(className: ClassName, methodSignature: JvmMethodSignature): Boolean +} + +/** + * Creates a new [ContainerData] instance for a given [className]. + * + * @param className the [ClassName] of the target class to to read from. + * @param parentClassName the parent [ClassName] name if [className] is nested, inner, or is a + * companion object. + */ +@KotlinPoetMetadataPreview +public fun ClassInspector.containerData( + className: ClassName, + parentClassName: ClassName?, +): ContainerData { + return containerData(declarationContainerFor(className), className, parentClassName) +} + +/** + * Looks up other classes, such as for nested members. Note that this class would always be + * Kotlin, so Metadata can be relied on for this. + * + * @param className The [ClassName] representation of the class. + * @return the read [KmClass] from its metadata. If no class was found, this should throw + * an exception. + */ +@KotlinPoetMetadataPreview +public fun ClassInspector.classFor(className: ClassName): KmClass { + val container = declarationContainerFor(className) + check(container is KmClass) { + "Container is not a class! Was ${container.javaClass.simpleName}" + } + return container +} diff --git a/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/ConstructorData.kt b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/ConstructorData.kt new file mode 100644 index 00000000..01c98390 --- /dev/null +++ b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/ConstructorData.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * 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 + * + * https://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.squareup.kotlinpoet.metadata.specs + +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview +import com.squareup.kotlinpoet.metadata.classinspectors.ClassInspectorUtil + +/** + * Represents relevant information on a constructor used for [ClassInspector]. Should only be + * associated with constructors of a [ClassData]. + * + * @param annotations declared annotations on this constructor. + * @property parameterAnnotations a mapping of parameter indices to annotations on them. + * @property isSynthetic indicates if this constructor is synthetic or not. + * @property jvmModifiers set of [JvmMethodModifiers][JvmMethodModifier] on this constructor. + * @property exceptions list of exceptions thrown by this constructor. + */ +@KotlinPoetMetadataPreview +public data class ConstructorData( + private val annotations: List<AnnotationSpec>, + val parameterAnnotations: Map<Int, Collection<AnnotationSpec>>, + val isSynthetic: Boolean, + val jvmModifiers: Set<JvmMethodModifier>, + val exceptions: List<TypeName>, +) { + + /** + * A collection of all annotations on this constructor, including any derived from [jvmModifiers], + * [isSynthetic], and [exceptions]. + */ + val allAnnotations: Collection<AnnotationSpec> = ClassInspectorUtil.createAnnotations { + addAll(annotations) + if (isSynthetic) { + add(ClassInspectorUtil.JVM_SYNTHETIC_SPEC) + } + addAll(jvmModifiers.mapNotNull { it.annotationSpec() }) + exceptions.takeIf { it.isNotEmpty() } + ?.let { + add(ClassInspectorUtil.createThrowsSpec(it)) + } + } + + public companion object { + public val EMPTY: ConstructorData = ConstructorData( + annotations = emptyList(), + parameterAnnotations = emptyMap(), + isSynthetic = false, + jvmModifiers = emptySet(), + exceptions = emptyList(), + ) + } +} diff --git a/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/ContainerData.kt b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/ContainerData.kt new file mode 100644 index 00000000..f1006d01 --- /dev/null +++ b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/ContainerData.kt @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * 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 + * + * https://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.squareup.kotlinpoet.metadata.specs + +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview +import kotlinx.metadata.KmClass +import kotlinx.metadata.KmConstructor +import kotlinx.metadata.KmDeclarationContainer +import kotlinx.metadata.KmFunction +import kotlinx.metadata.KmPackage +import kotlinx.metadata.KmProperty + +/** + * Represents relevant information on a declaration container used for [ClassInspector]. Can only + * ever be applied on a Kotlin type (i.e. is annotated with [Metadata]). + * + * @property declarationContainer the [KmDeclarationContainer] as parsed from the class's + * [@Metadata][Metadata] annotation. + * @property annotations declared annotations on this class. + * @property properties the mapping of [declarationContainer]'s properties to parsed [PropertyData]. + * @property methods the mapping of [declarationContainer]'s methods to parsed [MethodData]. + */ +@KotlinPoetMetadataPreview +public interface ContainerData { + public val declarationContainer: KmDeclarationContainer + public val annotations: Collection<AnnotationSpec> + public val properties: Map<KmProperty, PropertyData> + public val methods: Map<KmFunction, MethodData> +} + +/** + * Represents relevant information on a Kotlin class used for [ClassInspector]. Can only ever be + * applied on a class and not file facades. + * + * @property declarationContainer the [KmClass] as parsed from the class's + * [@Metadata][Metadata] annotation. + * @property className the KotlinPoet [ClassName] of the class. + * @property constructors the mapping of [declarationContainer]'s constructors to parsed + * [ConstructorData]. + */ +@KotlinPoetMetadataPreview +public data class ClassData( + override val declarationContainer: KmClass, + val className: ClassName, + override val annotations: Collection<AnnotationSpec>, + override val properties: Map<KmProperty, PropertyData>, + val constructors: Map<KmConstructor, ConstructorData>, + override val methods: Map<KmFunction, MethodData>, +) : ContainerData + +/** + * Represents relevant information on a file facade used for [ClassInspector]. + * + * @property declarationContainer the [KmClass] as parsed from the class's + * [@Metadata][Metadata] annotation. + * @property className the KotlinPoet [ClassName] of the underlying facade class in JVM. + * @property jvmName the `@JvmName` of the class or null if it does not have a custom name. + * Default will try to infer from the [className]. + */ +@KotlinPoetMetadataPreview +public data class FileData( + override val declarationContainer: KmPackage, + override val annotations: Collection<AnnotationSpec>, + override val properties: Map<KmProperty, PropertyData>, + override val methods: Map<KmFunction, MethodData>, + val className: ClassName, + val jvmName: String? = + if (!className.simpleName.endsWith("Kt")) className.simpleName else null, +) : ContainerData { + + /** + * The file name of the container, defaults to [className]'s simple name + "Kt". If a [jvmName] is + * specified, it will always defer to that. + */ + val fileName: String = jvmName ?: className.simpleName.removeSuffix("Kt") +} + +/** + * Represents relevant information on a Kotlin enum entry. + * + * @property declarationContainer the [KmClass] as parsed from the entry's + * [@Metadata][Metadata] annotation. + * @property annotations the annotations for the entry + */ +@KotlinPoetMetadataPreview +public data class EnumEntryData( + val declarationContainer: KmClass?, + val annotations: Collection<AnnotationSpec>, +) diff --git a/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/FieldData.kt b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/FieldData.kt new file mode 100644 index 00000000..a000ddef --- /dev/null +++ b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/FieldData.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * 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 + * + * https://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.squareup.kotlinpoet.metadata.specs + +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.AnnotationSpec.UseSiteTarget.FIELD +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview +import com.squareup.kotlinpoet.metadata.classinspectors.ClassInspectorUtil + +/** + * Represents relevant information on a field used for [ClassInspector]. Should only be + * associated with a [PropertyData]. + * + * @param annotations declared annotations on this field. + * @property isSynthetic indicates if this field is synthetic or not. + * @property jvmModifiers set of [JvmMethodModifiers][JvmMethodModifier] on this field. + * @property constant the constant value of this field, if available. Note that this is does not + * strictly imply that the associated property is `const`. + */ +@KotlinPoetMetadataPreview +public data class FieldData( + private val annotations: List<AnnotationSpec>, + val isSynthetic: Boolean, + val jvmModifiers: Set<JvmFieldModifier>, + val constant: CodeBlock?, +) { + + /** + * A collection of all annotations on this method, including any derived from [jvmModifiers] + * and [isSynthetic]. + */ + val allAnnotations: Collection<AnnotationSpec> = ClassInspectorUtil.createAnnotations( + FIELD, + ) { + addAll(annotations) + if (isSynthetic) { + add(ClassInspectorUtil.JVM_SYNTHETIC_SPEC) + } + addAll(jvmModifiers.mapNotNull(JvmFieldModifier::annotationSpec)) + } + + public companion object { + public val SYNTHETIC: FieldData = FieldData( + annotations = emptyList(), + isSynthetic = true, + jvmModifiers = emptySet(), + constant = null, + ) + } +} diff --git a/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/JvmFieldModifier.kt b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/JvmFieldModifier.kt new file mode 100644 index 00000000..dc2a55bb --- /dev/null +++ b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/JvmFieldModifier.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * 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 + * + * https://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.squareup.kotlinpoet.metadata.specs + +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.asClassName +import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview + +/** Modifiers that are annotations in Kotlin but modifier keywords in bytecode. */ +@KotlinPoetMetadataPreview +public enum class JvmFieldModifier : JvmModifier { + STATIC { + override fun annotationSpec(): AnnotationSpec = AnnotationSpec.builder( + JvmStatic::class.asClassName(), + ).build() + }, + TRANSIENT { + override fun annotationSpec(): AnnotationSpec = AnnotationSpec.builder( + Transient::class.asClassName(), + ).build() + }, + VOLATILE { + override fun annotationSpec(): AnnotationSpec = AnnotationSpec.builder( + Volatile::class.asClassName(), + ).build() + }, +} diff --git a/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/JvmMethodModifier.kt b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/JvmMethodModifier.kt new file mode 100644 index 00000000..5d63738b --- /dev/null +++ b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/JvmMethodModifier.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * 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 + * + * https://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.squareup.kotlinpoet.metadata.specs + +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.asClassName +import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview + +/** Modifiers that are annotations or implicit in Kotlin but modifier keywords in bytecode. */ +@KotlinPoetMetadataPreview +public enum class JvmMethodModifier : JvmModifier { + STATIC { + override fun annotationSpec(): AnnotationSpec = AnnotationSpec.builder( + JvmStatic::class.asClassName(), + ).build() + }, + SYNCHRONIZED { + override fun annotationSpec(): AnnotationSpec = AnnotationSpec.builder( + Synchronized::class.asClassName(), + ).build() + }, + DEFAULT, +} diff --git a/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/JvmModifier.kt b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/JvmModifier.kt new file mode 100644 index 00000000..f09e6add --- /dev/null +++ b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/JvmModifier.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * 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 + * + * https://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.squareup.kotlinpoet.metadata.specs + +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview + +/** + * Represents a JVM modifier that is represented as an annotation in Kotlin but as a modifier in + * bytecode. Examples include annotations such as [@JvmStatic][JvmStatic] or + * [@JvmSynthetic][JvmSynthetic]. + * + * This API is considered read-only and should not be implemented outside of KotlinPoet. + */ +@KotlinPoetMetadataPreview +public interface JvmModifier { + public fun annotationSpec(): AnnotationSpec? { + return null + } +} diff --git a/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/KmTypes.kt b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/KmTypes.kt new file mode 100644 index 00000000..cd12479c --- /dev/null +++ b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/KmTypes.kt @@ -0,0 +1,294 @@ +/* + * Copyright (C) 2019 Square, Inc. + * + * 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 + * + * https://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.squareup.kotlinpoet.metadata.specs + +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.LambdaTypeName +import com.squareup.kotlinpoet.ParameterizedTypeName +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.STAR +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.TypeVariableName +import com.squareup.kotlinpoet.WildcardTypeName +import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview +import com.squareup.kotlinpoet.metadata.classinspectors.ClassInspectorUtil +import com.squareup.kotlinpoet.metadata.isNullable +import com.squareup.kotlinpoet.metadata.isPrimary +import com.squareup.kotlinpoet.metadata.isReified +import com.squareup.kotlinpoet.metadata.isSuspend +import com.squareup.kotlinpoet.tags.TypeAliasTag +import kotlinx.metadata.KmClass +import kotlinx.metadata.KmClassifier +import kotlinx.metadata.KmClassifier.Class +import kotlinx.metadata.KmClassifier.TypeAlias +import kotlinx.metadata.KmClassifier.TypeParameter +import kotlinx.metadata.KmConstructor +import kotlinx.metadata.KmFlexibleTypeUpperBound +import kotlinx.metadata.KmFunction +import kotlinx.metadata.KmProperty +import kotlinx.metadata.KmType +import kotlinx.metadata.KmTypeParameter +import kotlinx.metadata.KmTypeProjection +import kotlinx.metadata.KmVariance +import kotlinx.metadata.KmVariance.IN +import kotlinx.metadata.KmVariance.INVARIANT +import kotlinx.metadata.KmVariance.OUT +import kotlinx.metadata.jvm.annotations +import kotlinx.metadata.jvm.signature + +/** + * `true` if this is an extension type (i.e. String.() -> Unit vs (String) -> Unit). + * + * See details: https://discuss.kotlinlang.org/t/announcing-kotlinx-metadata-jvm-library-for-reading-modifying-metadata-of-kotlin-jvm-class-files/7980/27 + */ +public val KmType.isExtensionType: Boolean get() { + return annotations.any { it.className == "kotlin/ExtensionFunctionType" } +} + +@KotlinPoetMetadataPreview +internal val KmClass.primaryConstructor: KmConstructor? + get() = constructors.find { it.isPrimary } + +internal fun KmVariance.toKModifier(): KModifier? { + return when (this) { + IN -> KModifier.IN + OUT -> KModifier.OUT + INVARIANT -> null + } +} + +@KotlinPoetMetadataPreview +internal fun KmTypeProjection.toTypeName( + typeParamResolver: TypeParameterResolver, +): TypeName { + val typename = type?.toTypeName(typeParamResolver) ?: STAR + return when (variance) { + IN -> WildcardTypeName.consumerOf(typename) + OUT -> WildcardTypeName.producerOf(typename) + INVARIANT -> typename + null -> STAR + } +} + +/** + * Converts a given [KmType] into a KotlinPoet representation, attempting to give a correct + * "source" representation. This includes converting [functions][kotlin.Function] and `suspend` + * types to appropriate [lambda representations][LambdaTypeName]. + */ +@KotlinPoetMetadataPreview +internal fun KmType.toTypeName( + typeParamResolver: TypeParameterResolver, +): TypeName { + val argumentList = arguments.map { it.toTypeName(typeParamResolver) } + val type: TypeName = when (val valClassifier = classifier) { + is TypeParameter -> { + typeParamResolver[valClassifier.id] + } + is KmClassifier.Class -> { + flexibleTypeUpperBound?.toTypeName(typeParamResolver)?.let { return it } + outerType?.toTypeName(typeParamResolver)?.let { return it } + var finalType: TypeName = ClassInspectorUtil.createClassName(valClassifier.name) + if (argumentList.isNotEmpty()) { + val finalTypeString = finalType.toString() + if (finalTypeString.startsWith("kotlin.Function")) { + // It's a lambda type! + finalType = if (finalTypeString == "kotlin.FunctionN") { + TODO("unclear how to express this one since it has arity") + } else { + val (parameters, returnType) = if (isSuspend) { + // Coroutines always adds an `Any?` return type, but we kind of just want the + // source representation, so we trick it here and ignore the last. + argumentList.dropLast(2).toTypedArray() to argumentList.dropLast(1).last().let { + // Coroutines makes these a `Continuation<T>` of the type, so we want the parameterized type + check(it is ParameterizedTypeName) + it.typeArguments[0] + } + } else { + argumentList.dropLast(1).toTypedArray() to argumentList.last() + } + val lambdaType = if (isExtensionType) { + // Extension function type! T.(). First parameter is actually the receiver. + LambdaTypeName.get( + receiver = parameters[0], + parameters = parameters.drop(1).toTypedArray(), + returnType = returnType, + ) + } else { + LambdaTypeName.get( + receiver = null, + parameters = parameters, + returnType = returnType, + ) + } + lambdaType.copy(suspending = isSuspend) + } + } else { + finalType = (finalType as ClassName).parameterizedBy(argumentList) + } + } + finalType + } + is TypeAlias -> { + ClassInspectorUtil.createClassName(valClassifier.name) + } + } + + val annotations = ClassInspectorUtil.createAnnotations { + for (annotation in annotations) { + add(annotation.toAnnotationSpec()) + } + }.toList() + val finalType = type.copy(nullable = isNullable, annotations = annotations) + return abbreviatedType?.let { + // This is actually an alias! The "abbreviated type" is the alias and how it's actually + // represented in source. So instead - we'll return the abbreviated type but store the "real" + // type in tags for reference. + val abbreviatedTypeName = it.toTypeName(typeParamResolver) + abbreviatedTypeName.copy( + tags = mapOf(TypeAliasTag::class to TypeAliasTag(finalType)), + ) + } ?: finalType +} + +@KotlinPoetMetadataPreview +internal fun KmTypeParameter.toTypeVariableName( + typeParamResolver: TypeParameterResolver, +): TypeVariableName { + val finalVariance = variance.toKModifier() + val typeVariableName = TypeVariableName( + name = name, + bounds = upperBounds.map { it.toTypeName(typeParamResolver) }, + variance = finalVariance, + ) + val annotations = ClassInspectorUtil.createAnnotations { + for (annotation in annotations) { + add(annotation.toAnnotationSpec()) + } + }.toList() + return typeVariableName.copy( + reified = isReified, + tags = mapOf(KmTypeParameter::class to this), + annotations = annotations, + ) +} + +@KotlinPoetMetadataPreview +private fun KmFlexibleTypeUpperBound.toTypeName( + typeParamResolver: TypeParameterResolver, +): TypeName { + // TODO tag typeFlexibilityId somehow? + return WildcardTypeName.producerOf(type.toTypeName(typeParamResolver)) +} + +internal interface TypeParameterResolver { + val parametersMap: Map<Int, TypeVariableName> + operator fun get(index: Int): TypeVariableName + + companion object { + val EMPTY = object : TypeParameterResolver { + override val parametersMap: Map<Int, TypeVariableName> = emptyMap() + + override fun get(index: Int): TypeVariableName = throw NoSuchElementException("No type parameters!") + } + } +} + +@KotlinPoetMetadataPreview +internal fun List<KmTypeParameter>.toTypeParameterResolver( + fallback: TypeParameterResolver? = null, +): TypeParameterResolver { + val parametersMap = LinkedHashMap<Int, TypeVariableName>() + val typeParamResolver = { id: Int -> + parametersMap[id] + ?: fallback?.get(id) + ?: throw IllegalStateException("No type argument found for $id!") + } + + val resolver = object : TypeParameterResolver { + override val parametersMap: Map<Int, TypeVariableName> = parametersMap + + override operator fun get(index: Int): TypeVariableName = typeParamResolver(index) + } + + // Fill the parametersMap. Need to do sequentially and allow for referencing previously defined params + for (typeParam in this) { + // Put the simple typevar in first, then it can be referenced in the full toTypeVariable() + // replacement later that may add bounds referencing this. + parametersMap[typeParam.id] = TypeVariableName(typeParam.name) + } + + for (typeParam in this) { + // Now replace it with the full version. + parametersMap[typeParam.id] = typeParam.toTypeVariableName(resolver) + } + + return resolver +} + +internal val KM_PROPERTY_COMPARATOR = Comparator<KmProperty> { o1, o2 -> + // No need to check fields, getters, etc as properties must have distinct names + o1.name.compareTo(o2.name) +} + +internal val KM_FUNCTION_COMPARATOR = Comparator<KmFunction> { o1, o2 -> + var result = o1.name.compareTo(o2.name) + if (result != 0) return@Comparator result + + val signature1 = o1.signature + val signature2 = o2.signature + if (signature1 != null && signature2 != null) { + result = signature1.asString().compareTo(signature2.asString()) + if (result != 0) return@Comparator result + } + + // Fallback - calculate signature + val manualSignature1 = o1.computeSignature() + val manualSignature2 = o2.computeSignature() + manualSignature1.compareTo(manualSignature2) +} + +internal val KM_CONSTRUCTOR_COMPARATOR = Comparator<KmConstructor> { o1, o2 -> + val signature1 = o1.signature + val signature2 = o2.signature + if (signature1 != null && signature2 != null) { + val result = signature1.asString().compareTo(signature2.asString()) + if (result != 0) return@Comparator result + } + + // Fallback - calculate signature + val manualSignature1 = o1.computeSignature() + val manualSignature2 = o2.computeSignature() + manualSignature1.compareTo(manualSignature2) +} + +// Computes a simple signature string good enough for hashing +private fun KmFunction.computeSignature(): String { + return "$name(${valueParameters.joinToString(",") { it.type.simpleName }})${returnType.simpleName}" +} + +private fun KmConstructor.computeSignature(): String { + return "$<init>(${valueParameters.joinToString(",") { it.type.simpleName }})" +} + +private val KmType?.simpleName: String get() { + if (this == null) return "void" + return when (val c = classifier) { + is Class -> c.name + is TypeParameter -> "Object" + is TypeAlias -> c.name + } +} diff --git a/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/KotlinPoetMetadataSpecs.kt b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/KotlinPoetMetadataSpecs.kt new file mode 100644 index 00000000..6c0730a6 --- /dev/null +++ b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/KotlinPoetMetadataSpecs.kt @@ -0,0 +1,948 @@ +/* + * Copyright (C) 2019 Square, Inc. + * + * 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 + * + * https://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. + */ +@file:JvmName("KotlinPoetMetadataSpecs") + +package com.squareup.kotlinpoet.metadata.specs + +import com.squareup.kotlinpoet.ANY +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.AnnotationSpec.UseSiteTarget +import com.squareup.kotlinpoet.AnnotationSpec.UseSiteTarget.FILE +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.FunSpec.Builder +import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.KModifier.ABSTRACT +import com.squareup.kotlinpoet.KModifier.CONST +import com.squareup.kotlinpoet.KModifier.CROSSINLINE +import com.squareup.kotlinpoet.KModifier.DATA +import com.squareup.kotlinpoet.KModifier.EXPECT +import com.squareup.kotlinpoet.KModifier.EXTERNAL +import com.squareup.kotlinpoet.KModifier.FINAL +import com.squareup.kotlinpoet.KModifier.INFIX +import com.squareup.kotlinpoet.KModifier.INLINE +import com.squareup.kotlinpoet.KModifier.INNER +import com.squareup.kotlinpoet.KModifier.INTERNAL +import com.squareup.kotlinpoet.KModifier.LATEINIT +import com.squareup.kotlinpoet.KModifier.NOINLINE +import com.squareup.kotlinpoet.KModifier.OPEN +import com.squareup.kotlinpoet.KModifier.OPERATOR +import com.squareup.kotlinpoet.KModifier.PRIVATE +import com.squareup.kotlinpoet.KModifier.PROTECTED +import com.squareup.kotlinpoet.KModifier.PUBLIC +import com.squareup.kotlinpoet.KModifier.SEALED +import com.squareup.kotlinpoet.KModifier.SUSPEND +import com.squareup.kotlinpoet.KModifier.TAILREC +import com.squareup.kotlinpoet.KModifier.VALUE +import com.squareup.kotlinpoet.KModifier.VARARG +import com.squareup.kotlinpoet.MemberName +import com.squareup.kotlinpoet.ParameterSpec +import com.squareup.kotlinpoet.PropertySpec +import com.squareup.kotlinpoet.TypeAliasSpec +import com.squareup.kotlinpoet.TypeSpec +import com.squareup.kotlinpoet.UNIT +import com.squareup.kotlinpoet.asClassName +import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview +import com.squareup.kotlinpoet.metadata.PropertyAccessorFlag +import com.squareup.kotlinpoet.metadata.PropertyAccessorFlag.IS_EXTERNAL +import com.squareup.kotlinpoet.metadata.PropertyAccessorFlag.IS_INLINE +import com.squareup.kotlinpoet.metadata.PropertyAccessorFlag.IS_NOT_DEFAULT +import com.squareup.kotlinpoet.metadata.classinspectors.ClassInspectorUtil +import com.squareup.kotlinpoet.metadata.classinspectors.ClassInspectorUtil.createAnnotations +import com.squareup.kotlinpoet.metadata.classinspectors.ClassInspectorUtil.createClassName +import com.squareup.kotlinpoet.metadata.classinspectors.ClassInspectorUtil.toTreeSet +import com.squareup.kotlinpoet.metadata.declaresDefaultValue +import com.squareup.kotlinpoet.metadata.hasAnnotations +import com.squareup.kotlinpoet.metadata.hasGetter +import com.squareup.kotlinpoet.metadata.hasSetter +import com.squareup.kotlinpoet.metadata.isAbstract +import com.squareup.kotlinpoet.metadata.isAnnotation +import com.squareup.kotlinpoet.metadata.isClass +import com.squareup.kotlinpoet.metadata.isCompanionObject +import com.squareup.kotlinpoet.metadata.isConst +import com.squareup.kotlinpoet.metadata.isCrossInline +import com.squareup.kotlinpoet.metadata.isData +import com.squareup.kotlinpoet.metadata.isDeclaration +import com.squareup.kotlinpoet.metadata.isDelegated +import com.squareup.kotlinpoet.metadata.isDelegation +import com.squareup.kotlinpoet.metadata.isEnum +import com.squareup.kotlinpoet.metadata.isEnumEntry +import com.squareup.kotlinpoet.metadata.isExpect +import com.squareup.kotlinpoet.metadata.isExternal +import com.squareup.kotlinpoet.metadata.isFinal +import com.squareup.kotlinpoet.metadata.isFun +import com.squareup.kotlinpoet.metadata.isInfix +import com.squareup.kotlinpoet.metadata.isInline +import com.squareup.kotlinpoet.metadata.isInner +import com.squareup.kotlinpoet.metadata.isInterface +import com.squareup.kotlinpoet.metadata.isInternal +import com.squareup.kotlinpoet.metadata.isLateinit +import com.squareup.kotlinpoet.metadata.isNoInline +import com.squareup.kotlinpoet.metadata.isObject +import com.squareup.kotlinpoet.metadata.isOpen +import com.squareup.kotlinpoet.metadata.isOperator +import com.squareup.kotlinpoet.metadata.isPrimary +import com.squareup.kotlinpoet.metadata.isPrivate +import com.squareup.kotlinpoet.metadata.isProtected +import com.squareup.kotlinpoet.metadata.isPublic +import com.squareup.kotlinpoet.metadata.isReified +import com.squareup.kotlinpoet.metadata.isSealed +import com.squareup.kotlinpoet.metadata.isSuspend +import com.squareup.kotlinpoet.metadata.isSynthesized +import com.squareup.kotlinpoet.metadata.isTailRec +import com.squareup.kotlinpoet.metadata.isVal +import com.squareup.kotlinpoet.metadata.isValue +import com.squareup.kotlinpoet.metadata.isVar +import com.squareup.kotlinpoet.metadata.propertyAccessorFlags +import com.squareup.kotlinpoet.metadata.specs.JvmMethodModifier.DEFAULT +import com.squareup.kotlinpoet.metadata.toKmClass +import com.squareup.kotlinpoet.tag +import java.util.Locale +import javax.lang.model.element.Element +import javax.lang.model.element.ElementKind +import javax.lang.model.element.PackageElement +import javax.lang.model.element.TypeElement +import kotlin.reflect.KClass +import kotlinx.metadata.Flags +import kotlinx.metadata.KmClass +import kotlinx.metadata.KmClassifier +import kotlinx.metadata.KmConstructor +import kotlinx.metadata.KmFunction +import kotlinx.metadata.KmPackage +import kotlinx.metadata.KmProperty +import kotlinx.metadata.KmType +import kotlinx.metadata.KmTypeAlias +import kotlinx.metadata.KmValueParameter +import kotlinx.metadata.jvm.JvmMethodSignature +import kotlinx.metadata.jvm.getterSignature +import kotlinx.metadata.jvm.jvmInternalName +import kotlinx.metadata.jvm.setterSignature +import kotlinx.metadata.jvm.signature + +/** @return a [TypeSpec] ABI representation of this [KClass]. */ +@KotlinPoetMetadataPreview +public fun KClass<*>.toTypeSpec( + classInspector: ClassInspector? = null, +): TypeSpec = java.toTypeSpec(classInspector) + +/** @return a [TypeSpec] ABI representation of this [KClass]. */ +@KotlinPoetMetadataPreview +public fun Class<*>.toTypeSpec( + classInspector: ClassInspector? = null, +): TypeSpec = toKmClass().toTypeSpec(classInspector, asClassName()) + +/** @return a [TypeSpec] ABI representation of this [TypeElement]. */ +@Suppress("DEPRECATION") +@KotlinPoetMetadataPreview +public fun TypeElement.toTypeSpec( + classInspector: ClassInspector? = null, +): TypeSpec = toKmClass().toTypeSpec(classInspector, asClassName()) + +/** @return a [FileSpec] ABI representation of this [KClass]. */ +@KotlinPoetMetadataPreview +public fun KClass<*>.toFileSpec( + classInspector: ClassInspector? = null, +): FileSpec = java.toFileSpec(classInspector) + +/** @return a [FileSpec] ABI representation of this [KClass]. */ +@KotlinPoetMetadataPreview +public fun Class<*>.toFileSpec( + classInspector: ClassInspector? = null, +): FileSpec = FileSpec.get(`package`.name, toTypeSpec(classInspector)) + +/** @return a [FileSpec] ABI representation of this [TypeElement]. */ +@KotlinPoetMetadataPreview +public fun TypeElement.toFileSpec( + classInspector: ClassInspector? = null, +): FileSpec = FileSpec.get( + packageName = packageName, + typeSpec = toTypeSpec(classInspector), +) + +/** @return a [TypeSpec] ABI representation of this [KmClass]. */ +@KotlinPoetMetadataPreview +public fun KmClass.toTypeSpec( + classInspector: ClassInspector?, + className: ClassName = createClassName(name), +): TypeSpec { + return toTypeSpec(classInspector, className, null) +} + +/** @return a [FileSpec] ABI representation of this [KmClass]. */ +@KotlinPoetMetadataPreview +public fun KmClass.toFileSpec( + classInspector: ClassInspector?, + className: ClassName = createClassName(name), +): FileSpec { + return FileSpec.get( + packageName = className.packageName, + typeSpec = toTypeSpec(classInspector, className), + ) +} + +/** @return a [FileSpec] ABI representation of this [KmPackage]. */ +@KotlinPoetMetadataPreview +public fun KmPackage.toFileSpec( + classInspector: ClassInspector?, + className: ClassName, +): FileSpec { + val fileData = classInspector?.containerData(className, null) + check(fileData is FileData?) { + "Unexpected container data type: ${fileData?.javaClass}" + } + val fileName = fileData?.fileName ?: className.simpleName + return FileSpec.builder(className.packageName, fileName) + .apply { + fileData?.let { data -> + data.jvmName?.let { name -> + addAnnotation( + AnnotationSpec.builder(ClassInspectorUtil.JVM_NAME) + .addMember("name = %S", name) + .build(), + ) + } + val fileAnnotations = createAnnotations(FILE) { + addAll(data.annotations.filterNot { it.typeName == METADATA }) + } + for (fileAnnotation in fileAnnotations) { + addAnnotation(fileAnnotation) + } + } + for (function in functions) { + val methodData = fileData?.methods?.get(function) + addFunction( + function.toFunSpec( + classInspector = classInspector, + containerData = fileData, + methodData = methodData, + isInInterface = false, + ), + ) + } + for (property in properties) { + val propertyData = fileData?.properties?.get(property) + addProperty( + property.toPropertySpec( + classInspector = classInspector, + containerData = fileData, + propertyData = propertyData, + isInInterface = false, + ), + ) + } + for (alias in typeAliases) { + addTypeAlias(alias.toTypeAliasSpec()) + } + } + .build() +} + +private const val NOT_IMPLEMENTED = "throw·NotImplementedError(\"Stub!\")" + +@KotlinPoetMetadataPreview +private fun KmClass.toTypeSpec( + classInspector: ClassInspector?, + className: ClassName, + parentClassName: ClassName?, +): TypeSpec { + val classTypeParamsResolver = typeParameters.toTypeParameterResolver() + val jvmInternalName = name.jvmInternalName + val simpleName = className.simpleName + val classData = classInspector?.containerData(className, parentClassName) + check(classData is ClassData?) { + "Unexpected container data type: ${classData?.javaClass}" + } + + val builder = when { + isAnnotation -> TypeSpec.annotationBuilder(simpleName) + isCompanionObject -> TypeSpec.companionObjectBuilder(companionObjectName(simpleName)) + isEnum -> TypeSpec.enumBuilder(simpleName) + isExpect -> TypeSpec.expectClassBuilder(simpleName) + isObject -> TypeSpec.objectBuilder(simpleName) + isInterface -> { + if (classData?.declarationContainer?.isFun == true) { + TypeSpec.funInterfaceBuilder(simpleName) + } else { + TypeSpec.interfaceBuilder(simpleName) + } + } + isEnumEntry -> TypeSpec.anonymousClassBuilder() + else -> TypeSpec.classBuilder(simpleName) + } + + classData?.annotations + ?.filterNot { + it.typeName == METADATA || it.typeName in JAVA_ANNOTATION_ANNOTATIONS + } + ?.let(builder::addAnnotations) + + if (isEnum) { + enumEntries.forEach { entryName -> + val typeSpec = if (classInspector != null) { + val entry = classInspector.enumEntry(className, entryName) + entry.declarationContainer + ?.let { enumEntryClass -> + val entryClassName = className.nestedClass(entryName) + enumEntryClass.toTypeSpec(classInspector, entryClassName, parentClassName = className) + } + ?: TypeSpec.anonymousClassBuilder() + .addAnnotations(entry.annotations) + .build() + } else { + TypeSpec.anonymousClassBuilder() + .addKdoc( + "No ClassInspector was available during metadata parsing, so this entry may not be reflected accurately if it has a class body.", + ) + .build() + } + builder.addEnumConstant(entryName, typeSpec) + } + } + + if (!isEnumEntry) { + visibilityFrom(flags) { builder.addModifiers(it) } + builder.addModifiers( + *flags.modalities + .filterNot { it == FINAL } // Default + .filterNot { isInterface && it == ABSTRACT } // Abstract is a default on interfaces + .toTypedArray(), + ) + if (isData) { + builder.addModifiers(DATA) + } + if (isExternal) { + builder.addModifiers(EXTERNAL) + } + if (isValue) { + builder.addModifiers(VALUE) + } + if (isInner) { + builder.addModifiers(INNER) + } + builder.addTypeVariables(typeParameters.map { it.toTypeVariableName(classTypeParamsResolver) }) + // If we have an inspector, we can check exactly which "supertype" is an interface vs + // class. Without a handler though, we have to best-effort guess. Usually, the flow is: + // - First element of a non-interface type is the superclass (can be `Any`) + // - First element of an interface type is the first superinterface + val superClassFilter = classInspector?.let { handler -> + { type: KmType -> + !handler.isInterface(createClassName((type.classifier as KmClassifier.Class).name)) + } + } ?: { true } + val superClass = supertypes.asSequence() + .filter { it.classifier is KmClassifier.Class } + .find(superClassFilter) + if (superClass != null && !isEnum && !isInterface && !isAnnotation) { + superClass.toTypeName(classTypeParamsResolver).takeIf { it != ANY } + ?.let(builder::superclass) + } + builder.addSuperinterfaces( + supertypes.asSequence() + .filterNot { it == superClass } + .map { it.toTypeName(classTypeParamsResolver) } + .filterNot { it == ANY } + .asIterable(), + ) + val primaryConstructorParams = mutableMapOf<String, ParameterSpec>() + if (isClass || isAnnotation || isEnum) { + primaryConstructor?.let { + it.toFunSpec(classTypeParamsResolver, classData?.constructors?.get(it) ?: return@let) + .also { spec -> + val finalSpec = if (isEnum && spec.annotations.isEmpty()) { + // Metadata specifies the constructor as private, but that's implicit so we can omit it + spec.toBuilder().apply { modifiers.remove(PRIVATE) }.build() + } else { + spec + } + builder.primaryConstructor(finalSpec) + primaryConstructorParams.putAll(spec.parameters.associateBy { it.name }) + } + } + constructors.filter { !it.isPrimary }.takeIf { it.isNotEmpty() }?.let { secondaryConstructors -> + builder.addFunctions( + secondaryConstructors + .mapNotNull { kmConstructor -> + classData?.constructors?.get(kmConstructor)?.let { kmConstructor to it } + } + .map { (kmConstructor, constructorData) -> + kmConstructor.toFunSpec(classTypeParamsResolver, constructorData) + }, + ) + } + } + builder.addProperties( + properties + .asSequence() + .filter { it.isDeclaration } + .filterNot { it.isSynthesized } + .map { it to classData?.properties?.get(it) } + .map { (property, propertyData) -> + property.toPropertySpec( + typeParamResolver = classTypeParamsResolver, + isConstructorParam = property.name in primaryConstructorParams, + classInspector = classInspector, + containerData = classData, + propertyData = propertyData, + ) + } + .asIterable(), + ) + companionObject?.let { objectName -> + val companionType = if (classInspector != null) { + val companionClassName = className.nestedClass(objectName) + classInspector.classFor(companionClassName) + .toTypeSpec(classInspector, companionClassName, parentClassName = className) + } else { + TypeSpec.companionObjectBuilder(companionObjectName(objectName)) + .addKdoc( + "No ClassInspector was available during metadata parsing, so this companion object's API/contents may not be reflected accurately.", + ) + .build() + } + builder.addType(companionType) + } + } + builder.addFunctions( + functions + .asSequence() + .filter { it.isDeclaration } + .filterNot { it.isDelegation } + .filterNot { it.isSynthesized } + .map { it to classData?.methods?.get(it) } + .map { (func, methodData) -> + func.toFunSpec(classTypeParamsResolver, classInspector, classData, methodData) + .toBuilder() + .apply { + // For interface methods, remove any body and mark the methods as abstract + fun isKotlinDefaultInterfaceMethod(): Boolean { + classInspector?.let { handler -> + func.signature?.let { signature -> + val suffix = signature.desc.removePrefix("(") + return handler.methodExists( + className.nestedClass("DefaultImpls"), + signature.copy( + desc = "(L$jvmInternalName;$suffix", + ), + ) + } + } + return false + } + // For interface methods, remove any body and mark the methods as abstract + // IFF it doesn't have a default interface body. + if (isInterface && + annotations.none { it.typeName == JVM_DEFAULT } && + (methodData?.jvmModifiers?.contains(DEFAULT) == false) && + !isKotlinDefaultInterfaceMethod() + ) { + addModifiers(ABSTRACT) + clearBody() + } else if (ABSTRACT in modifiers) { + // Remove bodies for abstract functions + clearBody() + } + if (methodData?.isSynthetic == true) { + addKdoc( + "Note: Since this is a synthetic function, some JVM information " + + "(annotations, modifiers) may be missing.", + ) + } + } + .build() + } + .asIterable(), + ) + + for (it in nestedClasses) { + val nestedClassName = className.nestedClass(it) + val nestedClass = classInspector?.classFor(nestedClassName) + val nestedType = if (nestedClass != null) { + if (nestedClass.isCompanionObject) { + // We handle these separately + continue + } else { + nestedClass.toTypeSpec(classInspector, nestedClassName, parentClassName = className) + } + } else { + TypeSpec.classBuilder(it) + .addKdoc( + "No ClassInspector was available during metadata parsing, so this nested class's API/contents may not be reflected accurately.", + ) + .build() + } + builder.addType(nestedType) + } + + return builder + .tag(this) + .build() +} + +private fun companionObjectName(name: String): String? { + return if (name == "Companion") null else name +} + +@KotlinPoetMetadataPreview +private fun KmConstructor.toFunSpec( + typeParamResolver: TypeParameterResolver, + constructorData: ConstructorData?, +): FunSpec { + return FunSpec.constructorBuilder() + .apply { + addAnnotations(constructorData?.allAnnotations.orEmpty()) + visibilityFrom(flags) { addModifiers(it) } + addParameters( + this@toFunSpec.valueParameters.mapIndexed { index, param -> + param.toParameterSpec( + typeParamResolver, + constructorData?.takeIf { it != ConstructorData.EMPTY } + ?.parameterAnnotations + ?.get(index) + .orEmpty(), + ) + }, + ) + if (!isPrimary) { + // TODO How do we know when to add callSuperConstructor()? + } + } + .tag(this) + .build() +} + +@KotlinPoetMetadataPreview +private val ContainerData.isInterface: Boolean get() { + return declarationContainer.let { container -> + container is KmClass && container.isInterface + } +} + +@KotlinPoetMetadataPreview +private fun KmFunction.toFunSpec( + classTypeParamsResolver: TypeParameterResolver = TypeParameterResolver.EMPTY, + classInspector: ClassInspector? = null, + containerData: ContainerData? = null, + methodData: MethodData? = null, + isInInterface: Boolean = containerData?.isInterface ?: false, +): FunSpec { + val typeParamsResolver = typeParameters.toTypeParameterResolver( + fallback = classTypeParamsResolver, + ) + val mutableAnnotations = mutableListOf<AnnotationSpec>() + if (classInspector != null && containerData != null) { + signature?.let { signature -> + if (!containerData.isInterface) { + // Infer if JvmName was used + // We skip interface types for this because they can't have @JvmName. + signature.jvmNameAnnotation(name)?.let { jvmNameAnnotation -> + mutableAnnotations += jvmNameAnnotation + } + } + } + } + val anyReified = typeParameters.any { it.isReified } + val isInFacade = containerData is FileData + val annotations = mutableAnnotations + .plus(methodData?.allAnnotations(containsReifiedTypeParameter = anyReified).orEmpty()) + .filterNot { isInFacade && it.typeName == JVM_STATIC } + .toTreeSet() + return FunSpec.builder(name) + .apply { + addAnnotations(annotations) + visibilityFrom(flags) { addModifiers(it) } + val isOverride = methodData?.isOverride == true + addModifiers( + flags.modalities + .filterNot { it == FINAL && !isOverride } // Final is the default + .filterNot { it == OPEN && isOverride } // Overrides are implicitly open + .filterNot { it == OPEN && isInInterface }, // interface methods are implicitly open + ) + if (valueParameters.isNotEmpty()) { + addParameters( + valueParameters.mapIndexed { index, param -> + param.toParameterSpec( + typeParamsResolver, + // This can be empty if the element is synthetic + methodData?.parameterAnnotations?.get(index).orEmpty(), + ) + }, + ) + } + if (typeParameters.isNotEmpty()) { + addTypeVariables(typeParameters.map { it.toTypeVariableName(typeParamsResolver) }) + } + if (methodData?.isOverride == true) { + addModifiers(KModifier.OVERRIDE) + } + if (isOperator) { + addModifiers(OPERATOR) + } + if (isInfix) { + addModifiers(INFIX) + } + if (isInline) { + addModifiers(INLINE) + } + if (isTailRec) { + addModifiers(TAILREC) + } + if (isExternal) { + addModifiers(EXTERNAL) + } + if (isExpect) { + addModifiers(EXPECT) + } + if (isSuspend) { + addModifiers(SUSPEND) + } + val returnTypeName = this@toFunSpec.returnType.toTypeName(typeParamsResolver) + if (returnTypeName != UNIT) { + returns(returnTypeName) + if (!flags.isAbstract) { + addStatement(NOT_IMPLEMENTED) + } + } + receiverParameterType?.toTypeName(typeParamsResolver)?.let { receiver(it) } + } + .tag(this) + .build() +} + +@KotlinPoetMetadataPreview +private fun KmValueParameter.toParameterSpec( + typeParamResolver: TypeParameterResolver, + annotations: Collection<AnnotationSpec>, +): ParameterSpec { + val paramType = varargElementType ?: type ?: throw IllegalStateException("No argument type!") + return ParameterSpec.builder(name, paramType.toTypeName(typeParamResolver)) + .apply { + addAnnotations(annotations) + if (varargElementType != null) { + addModifiers(VARARG) + } + if (isCrossInline) { + addModifiers(CROSSINLINE) + } + if (isNoInline) { + addModifiers(NOINLINE) + } + if (declaresDefaultValue) { + defaultValue(NOT_IMPLEMENTED) + } + } + .tag(this) + .build() +} + +@KotlinPoetMetadataPreview +private fun KmProperty.toPropertySpec( + typeParamResolver: TypeParameterResolver = TypeParameterResolver.EMPTY, + isConstructorParam: Boolean = false, + classInspector: ClassInspector? = null, + containerData: ContainerData? = null, + propertyData: PropertyData? = null, + isInInterface: Boolean = containerData?.isInterface ?: false, +): PropertySpec { + val isOverride = propertyData?.isOverride ?: false + val returnTypeName = returnType.toTypeName(typeParamResolver) + val mutableAnnotations = mutableListOf<AnnotationSpec>() + if (containerData != null && propertyData != null) { + if (hasGetter) { + getterSignature?.let { getterSignature -> + if (!containerData.isInterface && + !flags.isOpen && + !flags.isAbstract + ) { + // Infer if JvmName was used + // We skip interface types or open/abstract properties because they can't have @JvmName. + // For annotation properties, kotlinc puts JvmName annotations by default in + // bytecode but they're implicit in source, so we expect the simple name for + // annotation types. + val expectedMetadataName = if (containerData is ClassData && + containerData.declarationContainer.isAnnotation + ) { + name + } else { + "get${name.safeCapitalize(Locale.US)}" + } + getterSignature.jvmNameAnnotation( + metadataName = expectedMetadataName, + useSiteTarget = UseSiteTarget.GET, + )?.let { jvmNameAnnotation -> + mutableAnnotations += jvmNameAnnotation + } + } + } + } + if (hasSetter) { + setterSignature?.let { setterSignature -> + if (containerData is ClassData && + !containerData.declarationContainer.isAnnotation && + !containerData.declarationContainer.isInterface && + classInspector?.supportsNonRuntimeRetainedAnnotations == false && + !flags.isOpen && + !flags.isAbstract + ) { + // Infer if JvmName was used + // We skip annotation types for this because they can't have vars. + // We skip interface types or open/abstract properties because they can't have @JvmName. + setterSignature.jvmNameAnnotation( + metadataName = "set${name.safeCapitalize(Locale.US)}", + useSiteTarget = UseSiteTarget.SET, + )?.let { jvmNameAnnotation -> + mutableAnnotations += jvmNameAnnotation + } + } + } + } + } + return PropertySpec.builder(name, returnTypeName) + .apply { + // If a property annotation doesn't have a custom site target and is used in a constructor + // we have to add the property: site target to it. + + val isInFacade = containerData is FileData + val finalAnnotations = mutableAnnotations + .plus(propertyData?.allAnnotations.orEmpty()) + .filterNot { (isConst || isInFacade) && it.typeName == JVM_STATIC } + .map { + if (isConstructorParam && it.useSiteTarget == null) { + // TODO Ideally don't do this if the annotation use site is only field? + // e.g. JvmField. It's technically fine, but redundant on parameters as it's + // automatically applied to the property for these annotation types. + // This is another thing ClassInspector *could* tell us + it.toBuilder().useSiteTarget(UseSiteTarget.PROPERTY).build() + } else { + it + } + } + .toTreeSet() + addAnnotations(finalAnnotations) + visibilityFrom(flags) { addModifiers(it) } + addModifiers( + flags.modalities + .filterNot { it == FINAL && !isOverride } // Final is the default + .filterNot { it == OPEN && isOverride } // Overrides are implicitly open + .filterNot { it == OPEN && isInInterface } // Interface properties implicitly open + .filterNot { it == ABSTRACT && isInInterface }, // Interface properties implicitly abstract + ) + if (isOverride) { + addModifiers(KModifier.OVERRIDE) + } + if (isConst) { + addModifiers(CONST) + } + if (isVar) { + mutable(true) + } else if (isVal) { + mutable(false) + } + if (isDelegated) { + // Placeholders for these are tricky + addKdoc("Note: delegation is ABI stub only and not guaranteed to match source code.") + if (isVal) { + delegate("%M { %L }", MemberName("kotlin", "lazy"), NOT_IMPLEMENTED) // Placeholder + } else { + if (returnTypeName.isNullable) { + delegate( + "%T.observable(null) { _, _, _ -> }", + ClassName("kotlin.properties", "Delegates"), + ) + } else { + delegate("%T.notNull()", ClassName("kotlin.properties", "Delegates")) // Placeholder + } + } + } + if (isExpect) { + addModifiers(EXPECT) + } + if (isExternal) { + addModifiers(EXTERNAL) + } + if (isLateinit) { + addModifiers(LATEINIT) + } + if (isConstructorParam || (!isDelegated && !isLateinit)) { + val constant = propertyData?.fieldData?.constant + when { + constant != null -> initializer(constant) + isConstructorParam -> initializer(name) + returnTypeName.isNullable -> initializer("null") + flags.isAbstract || isInInterface -> { + // No-op, don't emit an initializer for abstract or interface properties + } + else -> initializer(NOT_IMPLEMENTED) + } + } + // Delegated properties have setters/getters defined for some reason, ignore here + // since the delegate handles it + // vals with initialized constants have a getter in bytecode but not a body in kotlin source + val modifierSet = modifiers.toSet() + if (hasGetter && !isDelegated && !flags.isAbstract) { + propertyAccessor( + modifierSet, + getterFlags, + FunSpec.getterBuilder().addStatement(NOT_IMPLEMENTED), + isOverride, + )?.let(::getter) + } + if (hasSetter && !isDelegated && !flags.isAbstract) { + propertyAccessor(modifierSet, setterFlags, FunSpec.setterBuilder(), isOverride)?.let(::setter) + } + } + .tag(this) + .build() +} + +@KotlinPoetMetadataPreview +private fun propertyAccessor( + propertyModifiers: Set<KModifier>, + flags: Flags, + functionBuilder: Builder, + isOverride: Boolean, +): FunSpec? { + val visibility = flags.visibility + if (visibility == PUBLIC || visibility !in propertyModifiers) { + // This is redundant and just a stub + // For annotations on this accessor, we declare them on the property with site target instead + return null + } + val modalities = flags.modalities + .filterNot { it == FINAL && !isOverride } + .filterNot { it == OPEN && isOverride } + val propertyAccessorFlags = flags.propertyAccessorFlags + return if (visibility != PUBLIC || modalities.isNotEmpty() || propertyAccessorFlags.isNotEmpty()) { + functionBuilder + .apply { + addModifiers(visibility) + addModifiers(modalities) + addModifiers(*propertyAccessorFlags.toKModifiersArray()) + } + .build() + } else { + null + } +} + +private fun Set<PropertyAccessorFlag>.toKModifiersArray(): Array<KModifier> { + return mapNotNull { + when (it) { + IS_EXTERNAL -> EXTERNAL + IS_INLINE -> INLINE + IS_NOT_DEFAULT -> null // Gracefully skip over these + } + }.toTypedArray() +} + +@KotlinPoetMetadataPreview +private fun KmTypeAlias.toTypeAliasSpec(): TypeAliasSpec { + val typeParamResolver = typeParameters.toTypeParameterResolver() + return TypeAliasSpec.builder(name, underlyingType.toTypeName(typeParamResolver)) + .apply { + visibilityFrom(flags) { + addModifiers(it) + } + if (flags.hasAnnotations) { + val annotationSpecs = this@toTypeAliasSpec.annotations + .map { it.toAnnotationSpec() } + addAnnotations(annotationSpecs) + } + } + .addTypeVariables(typeParamResolver.parametersMap.values) + .build() +} + +private fun JvmMethodSignature.jvmNameAnnotation( + metadataName: String, + useSiteTarget: UseSiteTarget? = null, +): AnnotationSpec? { + return if (name == metadataName) { + null + } else { + return AnnotationSpec.builder(JvmName::class) + .addMember("name = %S", name) + .useSiteTarget(useSiteTarget) + .build() + } +} + +private val JAVA_ANNOTATION_ANNOTATIONS = setOf( + java.lang.annotation.Retention::class.asClassName(), + java.lang.annotation.Target::class.asClassName(), +) + +@KotlinPoetMetadataPreview +private val Flags.visibility: KModifier + get() = when { + isInternal -> INTERNAL + isPrivate -> PRIVATE + isProtected -> PROTECTED + isPublic -> PUBLIC + else -> { + // IS_PRIVATE_TO_THIS or IS_LOCAL, so just default to public + PUBLIC + } + } + +@KotlinPoetMetadataPreview +private fun visibilityFrom(flags: Flags, body: (KModifier) -> Unit) { + val modifierVisibility = flags.visibility + if (modifierVisibility != PUBLIC) { + body(modifierVisibility) + } +} + +private fun String.safeCapitalize(locale: Locale): String { + return replaceFirstChar { if (it.isLowerCase()) it.titlecase(locale) else it.toString() } +} + +@KotlinPoetMetadataPreview +private val Flags.modalities: Set<KModifier> + get() = setOf { + if (isFinal) { + add(FINAL) + } + if (isOpen) { + add(OPEN) + } + if (isAbstract) { + add(ABSTRACT) + } + if (isSealed) { + add(SEALED) + } + } + +private inline fun <E> setOf(body: MutableSet<E>.() -> Unit): Set<E> { + return mutableSetOf<E>().apply(body).toSet() +} + +private val METADATA = Metadata::class.asClassName() + +@Suppress("DEPRECATION") +private val JVM_DEFAULT = JvmDefault::class.asClassName() +private val JVM_STATIC = JvmStatic::class.asClassName() + +@PublishedApi +internal val Element.packageName: String + get() { + var element = this + while (element.kind != ElementKind.PACKAGE) { + element = element.enclosingElement + } + return (element as PackageElement).toString() + } diff --git a/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/MethodData.kt b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/MethodData.kt new file mode 100644 index 00000000..7319e9d8 --- /dev/null +++ b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/MethodData.kt @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * 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 + * + * https://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.squareup.kotlinpoet.metadata.specs + +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.AnnotationSpec.UseSiteTarget +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview +import com.squareup.kotlinpoet.metadata.classinspectors.ClassInspectorUtil + +/** + * Represents relevant information on a method used for [ClassInspector]. Should only be + * associated with methods of a [ClassData] or [PropertyData]. + * + * @param annotations declared annotations on this method. + * @property parameterAnnotations a mapping of parameter indices to annotations on them. + * @property isSynthetic indicates if this method is synthetic or not. + * @property jvmModifiers set of [JvmMethodModifiers][JvmMethodModifier] on this method. + * @property isOverride indicates if this method overrides one in a supertype. + * @property exceptions list of exceptions thrown by this method. + */ +@KotlinPoetMetadataPreview +public data class MethodData( + private val annotations: List<AnnotationSpec>, + val parameterAnnotations: Map<Int, Collection<AnnotationSpec>>, + val isSynthetic: Boolean, + val jvmModifiers: Set<JvmMethodModifier>, + val isOverride: Boolean, + val exceptions: List<TypeName>, +) { + + /** + * A collection of all annotations on this method, including any derived from [jvmModifiers], + * [isSynthetic], and [exceptions]. + * + * @param useSiteTarget an optional [UseSiteTarget] that all annotations on this method should + * use. + * @param containsReifiedTypeParameter an optional boolean indicating if any type parameters on + * this function are `reified`, which are implicitly synthetic. + */ + public fun allAnnotations( + useSiteTarget: UseSiteTarget? = null, + containsReifiedTypeParameter: Boolean = false, + ): Collection<AnnotationSpec> { + return ClassInspectorUtil.createAnnotations( + useSiteTarget, + ) { + addAll(annotations) + if (isSynthetic && !containsReifiedTypeParameter) { + add(ClassInspectorUtil.JVM_SYNTHETIC_SPEC) + } + addAll(jvmModifiers.mapNotNull(JvmMethodModifier::annotationSpec)) + exceptions.takeIf { it.isNotEmpty() } + ?.let { + add(ClassInspectorUtil.createThrowsSpec(it, useSiteTarget)) + } + } + } + + public companion object { + public val SYNTHETIC: MethodData = MethodData( + annotations = emptyList(), + parameterAnnotations = emptyMap(), + isSynthetic = true, + jvmModifiers = emptySet(), + isOverride = false, + exceptions = emptyList(), + ) + public val EMPTY: MethodData = MethodData( + annotations = emptyList(), + parameterAnnotations = emptyMap(), + isSynthetic = false, + jvmModifiers = emptySet(), + isOverride = false, + exceptions = emptyList(), + ) + } +} diff --git a/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/PropertyData.kt b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/PropertyData.kt new file mode 100644 index 00000000..9fc6d3e4 --- /dev/null +++ b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/PropertyData.kt @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * 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 + * + * https://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.squareup.kotlinpoet.metadata.specs + +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.AnnotationSpec.UseSiteTarget.GET +import com.squareup.kotlinpoet.AnnotationSpec.UseSiteTarget.SET +import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview +import com.squareup.kotlinpoet.metadata.classinspectors.ClassInspectorUtil + +/** + * Represents relevant information on a property used for [ClassInspector]. Should only be + * associated with properties of a [ClassData]. + * + * @param annotations declared annotations on this property. + * @property fieldData associated [FieldData] with this property, if any. + * @property getterData associated getter (as [MethodData]) with this property, if any. + * @property setterData associated setter (as [MethodData]) with this property, if any. + * @property isJvmField indicates if this property should be treated as a jvm field. + */ +@KotlinPoetMetadataPreview +public data class PropertyData( + private val annotations: List<AnnotationSpec>, + val fieldData: FieldData?, + val getterData: MethodData?, + val setterData: MethodData?, + val isJvmField: Boolean, +) { + /** Indicates if this property overrides another from a supertype. */ + val isOverride: Boolean = (getterData?.isOverride ?: false) || (setterData?.isOverride ?: false) + + /** + * A collection of all annotations on this property including declared ones and any derived from + * [fieldData], [getterData], [setterData], and [isJvmField]. + */ + val allAnnotations: Collection<AnnotationSpec> = ClassInspectorUtil.createAnnotations { + // Don't add annotations that are already defined on the parent + val higherScopedAnnotations = annotations.associateBy { it.typeName } + val fieldAnnotations = fieldData?.allAnnotations.orEmpty() + .filterNot { it.typeName in higherScopedAnnotations } + .associateByTo(LinkedHashMap()) { it.typeName } + val getterAnnotations = getterData?.allAnnotations(GET).orEmpty() + .filterNot { it.typeName in higherScopedAnnotations } + .associateByTo(LinkedHashMap()) { it.typeName } + + val finalTopAnnotations = annotations.toMutableList() + + // If this is a val, and annotation is on both getter and field, we can move it to just the + // regular annotations + if (setterData == null && !isJvmField) { + val sharedAnnotations = getterAnnotations.keys.intersect(fieldAnnotations.keys) + for (sharedAnnotation in sharedAnnotations) { + // Add it to the top-level annotations without a site-target + finalTopAnnotations += getterAnnotations.getValue(sharedAnnotation).toBuilder() + .useSiteTarget(null) + .build() + + // Remove from field and getter + fieldAnnotations.remove(sharedAnnotation) + getterAnnotations.remove(sharedAnnotation) + } + } + + addAll(finalTopAnnotations) + addAll(fieldAnnotations.values) + addAll(getterAnnotations.values) + addAll( + setterData?.allAnnotations(SET).orEmpty() + .filterNot { it.typeName in higherScopedAnnotations }, + ) + if (isJvmField) { + add(ClassInspectorUtil.JVM_FIELD_SPEC) + } + } +} diff --git a/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/kmAnnotations.kt b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/kmAnnotations.kt new file mode 100644 index 00000000..f6b44670 --- /dev/null +++ b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/kmAnnotations.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * 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 + * + * https://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.squareup.kotlinpoet.metadata.specs + +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.joinToCode +import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview +import com.squareup.kotlinpoet.metadata.classinspectors.ClassInspectorUtil.createClassName +import com.squareup.kotlinpoet.tag +import kotlinx.metadata.KmAnnotation +import kotlinx.metadata.KmAnnotationArgument +import kotlinx.metadata.KmAnnotationArgument.AnnotationValue +import kotlinx.metadata.KmAnnotationArgument.ArrayValue +import kotlinx.metadata.KmAnnotationArgument.BooleanValue +import kotlinx.metadata.KmAnnotationArgument.ByteValue +import kotlinx.metadata.KmAnnotationArgument.CharValue +import kotlinx.metadata.KmAnnotationArgument.DoubleValue +import kotlinx.metadata.KmAnnotationArgument.EnumValue +import kotlinx.metadata.KmAnnotationArgument.FloatValue +import kotlinx.metadata.KmAnnotationArgument.IntValue +import kotlinx.metadata.KmAnnotationArgument.KClassValue +import kotlinx.metadata.KmAnnotationArgument.LongValue +import kotlinx.metadata.KmAnnotationArgument.ShortValue +import kotlinx.metadata.KmAnnotationArgument.StringValue +import kotlinx.metadata.KmAnnotationArgument.UByteValue +import kotlinx.metadata.KmAnnotationArgument.UIntValue +import kotlinx.metadata.KmAnnotationArgument.ULongValue +import kotlinx.metadata.KmAnnotationArgument.UShortValue + +@KotlinPoetMetadataPreview +internal fun KmAnnotation.toAnnotationSpec(): AnnotationSpec { + val cn = createClassName(className) + return AnnotationSpec.builder(cn) + .apply { + arguments.forEach { (name, arg) -> + addMember("%L = %L", name, arg.toCodeBlock()) + } + } + .tag(this) + .build() +} + +@OptIn(ExperimentalUnsignedTypes::class) +@KotlinPoetMetadataPreview +internal fun KmAnnotationArgument.toCodeBlock(): CodeBlock { + return when (this) { + is ByteValue -> CodeBlock.of("%L", value) + is CharValue -> CodeBlock.of("'%L'", value) + is ShortValue -> CodeBlock.of("%L", value) + is IntValue -> CodeBlock.of("%L", value) + is LongValue -> CodeBlock.of("%LL", value) + is FloatValue -> CodeBlock.of("%LF", value) + is DoubleValue -> CodeBlock.of("%L", value) + is BooleanValue -> CodeBlock.of("%L", value) + is UByteValue -> CodeBlock.of("%Lu", value) + is UShortValue -> CodeBlock.of("%Lu", value) + is UIntValue -> CodeBlock.of("%Lu", value) + is ULongValue -> CodeBlock.of("%Lu", value) + is StringValue -> CodeBlock.of("%S", value) + is KClassValue -> CodeBlock.of("%T::class", createClassName(className)) + is EnumValue -> CodeBlock.of("%T.%L", createClassName(enumClassName), enumEntryName) + is AnnotationValue -> CodeBlock.of("%L", annotation.toAnnotationSpec()) + is ArrayValue -> elements.map { it.toCodeBlock() }.joinToCode(", ", "[", "]") + } +} |