-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat!: generate library code for field operations on kotlin standard …
…library types (#273) * build: add code-generator module * feat: support runtime alignement check for an arbitrary field count * feat: add shortcut for getting to the set of neighbors from a field * porcaio * stash this commit * feat: add combinator * some minor * some minor * chore: updates * feat: add primitives code generator * feat: support the generation for property members * feat: generate a file for each specified type * feat: provide a dedicate function for code generation * test: provide minimal test for validating the generated code * refactor: use KClass instead hard-coded string for collektive classes * refactor: move Utils into codgen folder * docs: improve kdoc of Field * chore(deps): move dependency into catalog * build: sort dependencies * fix: do not generate field-based method for internal member * fix: include also the receiver in the checkAligned method * refactor: move into buildSrc the code generation * feat: implement field primitives code generator inside buildSrc * feat: introduce stdlib project * build: remove hard-coded Field class and use source set pointing to dsl project * docs: fix documentation comment * build: enable compiler plugin * build: enable the foojay resolver in buildSrc * build: enable the multi jvm test in buildSrc * feat(stdlib): add a header to the generated sources * style(codegen): improve readability * refactor(codegen): generate code with Java 8 and improve the namespacing * fixup: work around the limitations with recurring generic types * feat: simplify the type resolution * fix: exclude `associate`(`By`)`To` * chore: ignore .kotlin * fix: exclude `associateWithTo` * fix: exclude `joinTo` * fix: exclude `filterIndexedTo` * fix: exclude `filterNotTo` * fix: exclude `filterTo` * fix: exclude `binarySearch` * fix: exclude `map` and `mapTo` * fix: exclude `mapNotNullTo` * fix: exclude `mapIndexedTo` * fix: exclude `groupByTo` * fix: exclude `flatMapTo` * fix: exclude `mapIndexedNotNullTo` * fix(codegen): add star projection support * fix(codegen): retain `inline` and `infix` * feat(codegen): add support for `reified` type parameters in method signatures * fix(codegen): add support for `crossinline` function types * fix: solved error No type arguments expected for class *Array * feat(stdlib): split the generated code into multiple files based on the receiver type * feat(stdlib): generate type arguments for 0-ary functions * feat(stdlib): improve wildcard detection and rename * feat(stdlib): skip filterIsInstanceTo * feat(stdlib): skip filterNotNullTo * feat(stdlib): detect nullables in jvmnames * feat(stdlib): exclude generation for functions affected by square/kotlinpoet#1933 * feat(stdlib): make the generated code compile * fix(codegen): work around square/kotlinpoet#1933 * feat(codegen)!: avoid type variable names as package segments * refactor: code refactoring in appropriate utils objects --------- Co-authored-by: Danilo Pianini <[email protected]> Co-authored-by: Danilo Pianini <[email protected]> Co-authored-by: Angela Cortecchia <[email protected]>
- Loading branch information
1 parent
dc8d650
commit 31545f2
Showing
14 changed files
with
813 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,9 @@ | ||
.gradle/ | ||
build/ | ||
.idea/ | ||
.kotlin/ | ||
.vscode | ||
bin | ||
build/ | ||
kotlin-js-store | ||
*.hprof | ||
local.properties | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import com.squareup.kotlinpoet.asTypeName | ||
import kotlin.reflect.jvm.kotlinFunction | ||
|
||
fun main(args: Array<String>) { | ||
Class.forName("kotlin.collections.ArraysKt").methods.first { | ||
it.name == "max" && it.parameters.first().parameterizedType.typeName.contains("Comparable") | ||
}.kotlinFunction?.parameters?.first()?.type?.asTypeName() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import com.squareup.kotlinpoet.ClassName | ||
import com.squareup.kotlinpoet.FileSpec | ||
import com.squareup.kotlinpoet.FunSpec | ||
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy | ||
import com.squareup.kotlinpoet.TypeVariableName | ||
import com.squareup.kotlinpoet.WildcardTypeName | ||
|
||
fun <T> foo(): Array<out T> = TODO() | ||
|
||
fun main() { | ||
val funspec = FunSpec.builder("foo") | ||
.addTypeVariable(TypeVariableName("T")) | ||
.returns( | ||
ClassName.bestGuess("kotlin.Array") | ||
.parameterizedBy(WildcardTypeName.producerOf(TypeVariableName("T"))) | ||
) | ||
.addStatement("TODO()") | ||
.build() | ||
println( | ||
FileSpec.builder("foo.bar", "Baz") | ||
.addFunction(funspec) | ||
.build() | ||
.toString() | ||
) | ||
} |
51 changes: 51 additions & 0 deletions
51
buildSrc/src/main/kotlin/it/unibo/collektive/codegen/CollektiveCodegenTask.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
package it.unibo.collektive.codegen | ||
|
||
import org.gradle.api.tasks.JavaExec | ||
import org.gradle.api.tasks.OutputDirectory | ||
import org.gradle.api.tasks.TaskAction | ||
import org.gradle.jvm.toolchain.JavaLanguageVersion | ||
import java.io.File | ||
import java.net.URLClassLoader | ||
|
||
abstract class CollektiveCodegenTask : JavaExec() { | ||
@OutputDirectory | ||
lateinit var outputDir: File | ||
|
||
init { | ||
this.javaLauncher.set( | ||
javaToolchainService.launcherFor { | ||
languageVersion.set(JavaLanguageVersion.of(8)) | ||
} | ||
) | ||
mainClass.set(FieldedMembersGenerator::class.qualifiedName) | ||
} | ||
|
||
@TaskAction | ||
override fun exec() { | ||
args(outputDir.absolutePath) | ||
val classpath: Array<String> = sequenceOf(this::class.java.classLoader) | ||
//generateSequence(this::class.java.classLoader) { it.parent } | ||
.filterIsInstance<URLClassLoader>() | ||
.flatMap { it.urLs.asSequence() } | ||
.orEmpty() | ||
.mapNotNull { it.file.takeUnless { it.isNullOrBlank() } } | ||
.toList() | ||
.toTypedArray() | ||
check(classpath.isNotEmpty()) { | ||
"Classpath detection for the Collektive code generation task failed." | ||
} | ||
classpath(*classpath) | ||
outputDir.mkdirs() | ||
super.exec() | ||
} | ||
|
||
companion object { | ||
@JvmStatic | ||
fun main(args: Array<String>) { | ||
val outputDir = args[0] | ||
FieldedMembersGenerator.generateFieldFunctionsForTypes(FieldedMembersGenerator.baseTargetTypes).forEach { | ||
it.writeTo(File(outputDir)) | ||
} | ||
} | ||
} | ||
} |
247 changes: 247 additions & 0 deletions
247
buildSrc/src/main/kotlin/it/unibo/collektive/codegen/FieldedMembersGenerator.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,247 @@ | ||
package it.unibo.collektive.codegen | ||
|
||
import com.squareup.kotlinpoet.FileSpec | ||
import it.unibo.collektive.codegen.utils.generatePrimitivesFile | ||
import it.unibo.collektive.field.Field | ||
import java.io.File | ||
import kotlin.reflect.KCallable | ||
import kotlin.reflect.KClass | ||
import kotlin.reflect.KType | ||
import kotlin.reflect.KTypeParameter | ||
import kotlin.reflect.KVisibility | ||
import kotlin.reflect.full.extensionReceiverParameter | ||
import kotlin.reflect.full.isSubtypeOf | ||
import kotlin.reflect.jvm.kotlinFunction | ||
import kotlin.reflect.typeOf | ||
|
||
|
||
object FieldedMembersGenerator { | ||
|
||
val baseExtensions = sequenceOf( | ||
"Arrays", "Collections", "Sets", "Maps" | ||
).map { | ||
Class.forName("kotlin.collections.${it}Kt").kotlin | ||
} | ||
|
||
/** | ||
* The base types for which to generate field functions. | ||
*/ | ||
val baseTargetTypes = sequenceOf( | ||
// Boolean | ||
Boolean::class, | ||
// Numeric types | ||
Byte::class, | ||
Short::class, | ||
Int::class, | ||
Float::class, | ||
Double::class, | ||
Long::class, | ||
// Unsigned types | ||
UByte::class, | ||
UShort::class, | ||
UInt::class, | ||
ULong::class, | ||
// Chars and strings | ||
Char::class, | ||
CharSequence::class, | ||
String::class, | ||
// Collections | ||
Collection::class, | ||
Iterable::class, | ||
List::class, | ||
Map::class, | ||
Sequence::class, | ||
Set::class, | ||
// Other types | ||
Comparator::class, | ||
Pair::class, | ||
Triple::class, | ||
Result::class, | ||
// Function collections | ||
) + baseExtensions | ||
|
||
val mutablesAndSideEffects: List<KType> = listOf( | ||
typeOf<Array<*>>(), | ||
typeOf<MutableCollection<*>>(), | ||
typeOf<MutableCollection<*>>(), | ||
typeOf<MutableIterable<*>>(), | ||
typeOf<MutableIterator<*>>(), | ||
typeOf<MutableList<*>>(), | ||
typeOf<MutableListIterator<*>>(), | ||
typeOf<MutableMap<*, *>>(), | ||
typeOf<ShortArray>(), | ||
typeOf<IntArray>(), | ||
typeOf<FloatArray>(), | ||
typeOf<DoubleArray>(), | ||
typeOf<LongArray>(), | ||
typeOf<MutableSet<*>>(), | ||
typeOf<Unit>(), | ||
) | ||
|
||
fun KType.isMutable(): Boolean = mutablesAndSideEffects.any { isSubtypeOf(it) } | ||
|
||
fun KTypeParameter.isMutable() = upperBounds.any { bound -> bound.isMutable() } | ||
|
||
val forbiddenPrefixes = listOf( | ||
"java", | ||
"javax", | ||
) | ||
|
||
val permanentlyExcludedMemberNames = listOf( | ||
"associateByTo", | ||
"associateTo", | ||
"associateWithTo", | ||
"binarySearch", | ||
"clone", | ||
"copyInto", | ||
"dec", | ||
"filterIndexedTo", | ||
"filterIsInstanceTo", | ||
"filterNotNullTo", | ||
"filterNotTo", | ||
"filterTo", | ||
"flatMapTo", | ||
"fold", | ||
"foldIndexed", | ||
"foldIndexedOrNull", | ||
"foldLeft", | ||
"foldLeftIndexed", | ||
"foldOrNull", | ||
"foldRight", | ||
"foldRightIndexed", | ||
"groupByTo", | ||
"inc", | ||
"joinTo", | ||
"listOf", | ||
"listOfNotNull", | ||
"map", | ||
"mapIndexedNotNullTo", | ||
"mapIndexedTo", | ||
"mapNotNullTo", | ||
"mapOf", | ||
"mapTo", | ||
"reduce", | ||
"reduceIndexed", | ||
"reduceIndexedOrNull", | ||
"reduceLeft", | ||
"reduceLeftIndexed", | ||
"reduceOrNull", | ||
"reduceRight", | ||
"reduceRightIndexed", | ||
"readObject", | ||
"setOf", | ||
"setOfNotNull", | ||
"toBooleanArray", | ||
"toByteArray", | ||
"toCharArray", | ||
"toCollection", | ||
"toDoubleArray", | ||
"toFloatArray", | ||
"toIntArray", | ||
"toLongArray", | ||
"toShortArray", | ||
"toMutableMap", | ||
"toMutableList", | ||
"toMutableSet", | ||
"toTypedArray", | ||
"writeObject", | ||
) + listOf("fold", "reduce") | ||
.flatMap { listOf(it, "${it}Indexed", "${it}Left", "${it}Right", "${it}OrNull") } | ||
.flatMap { listOf(it, "${it}OrNull", "${it}Indexed") } | ||
.flatMap { listOf(it, "${it}OrNull") } | ||
|
||
/** | ||
* Generates field-based functions for the given types. | ||
*/ | ||
fun generateFieldFunctionsForTypes( | ||
types: Sequence<KClass<*>>, | ||
excludeMembers: List<String> = emptyList(), | ||
): Sequence<FileSpec> { | ||
fun KCallable<*>.paramTypes() = parameters.drop(1).map { it.type } | ||
val forbiddenMembers = Field::class.members.map { member -> member.name to member.paramTypes() } | ||
// "dec" and "inc" are excluded due to: KT-24800 | ||
val forbiddenMembersName = permanentlyExcludedMemberNames + excludeMembers | ||
return types.flatMap { clazz -> | ||
val javaMethods: Collection<KCallable<*>> = clazz.java.methods.mapNotNull { | ||
runCatching { it.kotlinFunction }.getOrNull() | ||
} | ||
val kotlinMembers: Collection<KCallable<*>> = clazz.members | ||
val allTargets = javaMethods.toSet() + kotlinMembers.toSet() | ||
val order = compareBy<KCallable<*>> { it.name } | ||
.thenBy { it.parameters.size } | ||
.thenBy { it.paramTypes().joinToString() } | ||
val sortedTargets = allTargets.sortedWith(order) | ||
val withValidAnnotations = sortedTargets.filter { callable -> | ||
callable.annotations.none { | ||
it is Deprecated || it.annotationClass.simpleName == "PlatformDependent" | ||
} | ||
} | ||
val public = withValidAnnotations.filter { it.visibility == KVisibility.PUBLIC } | ||
val returnTypeMeaningful = public.filterNot { | ||
forbiddenPrefixes.any { prefix -> it.returnType.toString().startsWith(prefix) } || | ||
mutablesAndSideEffects.any { mutable -> it.returnType.isSubtypeOf(mutable) } | ||
} | ||
val parametersMeaningful = returnTypeMeaningful.filterNot { method -> | ||
method.parameters.any { parameter -> | ||
forbiddenPrefixes.any { prefix -> parameter.type.toString().startsWith(prefix) } || | ||
when (val classifier = parameter.type.classifier) { | ||
null -> error("Null classifier in $method") | ||
is KClass<*> -> parameter.type.isMutable() | ||
is KTypeParameter -> classifier.isMutable() | ||
else -> error("Unknown classifier type ${classifier::class}") | ||
} | ||
} | ||
} | ||
val genericBoundsMeaningful = parametersMeaningful.filterNot { method -> | ||
method.typeParameters.any { typeParameter -> | ||
forbiddenPrefixes.any { prefix -> | ||
typeParameter.upperBounds.any { it.toString().startsWith(prefix) } | ||
} | ||
} | ||
} | ||
val noConflictingMethods = genericBoundsMeaningful.filterNot { callable -> | ||
callable.name to callable.paramTypes() in forbiddenMembers | ||
} | ||
val validMembers = noConflictingMethods | ||
.filterNot { it.name in forbiddenMembersName } | ||
.toList() | ||
val name = checkNotNull(clazz.simpleName) { | ||
"Cannot generate field functions for anonymous class $clazz" | ||
}.removeSuffix("Kt") | ||
val extensions: Map<String, List<KCallable<*>>> = validMembers.groupBy { callable -> | ||
fun KType?.cleanString(): String = toString().substringBefore('<').substringAfterLast('.') | ||
fun KTypeParameter.allUpperBounds(): List<KType> = upperBounds | ||
.flatMap { (it.classifier as? KTypeParameter)?.allUpperBounds() ?: listOf(it) } | ||
val receiver = callable.extensionReceiverParameter?.type | ||
when (val classifier = receiver?.classifier) { | ||
null -> name | ||
is KTypeParameter -> classifier.allUpperBounds() | ||
.map { it.cleanString() } | ||
.sorted() | ||
.joinToString(separator = "") | ||
else -> receiver.cleanString() | ||
} | ||
} | ||
extensions.asSequence() | ||
.filter { (_, members) -> members.isNotEmpty() } | ||
.mapNotNull { (receiver, members) -> | ||
generatePrimitivesFile( | ||
members, | ||
"it.unibo.collektive.stdlib.${receiver.lowercase()}s", | ||
"Fielded${name}${if (name.endsWith("s")) "Extensions" else 's'}", | ||
) | ||
} | ||
} | ||
} | ||
|
||
@JvmStatic | ||
fun main(args: Array<String>) { | ||
val outputDir = if (args.isEmpty()) null else args[0] | ||
generateFieldFunctionsForTypes(baseTargetTypes).forEach { source -> | ||
when { | ||
outputDir == null -> println(source) | ||
else -> source.writeTo(File(outputDir)) | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.