From d07b1b65020e730abc478275392e098c16d7c092 Mon Sep 17 00:00:00 2001 From: Sebastian Sellmair Date: Wed, 22 Mar 2023 16:59:25 +0100 Subject: [PATCH] [CLI] Implement CommonToolArguments.toStringList (to replace convertArgumentsToStringList`) KTIJ-24976 --- build-common/build.gradle.kts | 1 + .../kotlin/compilerRunner/ArgumentUtils.java | 87 +----------- .../compilerRunner/argumentsToStrings.kt | 91 +++++++++++++ .../CompilerArgumentParsingTest.kt | 124 ++++++++++++++++++ .../CompilerArgumentReflectionUtils.kt | 19 +++ .../CompilerArgumentsImplementationTest.kt | 40 ++++++ 6 files changed, 279 insertions(+), 83 deletions(-) create mode 100644 build-common/src/org/jetbrains/kotlin/compilerRunner/argumentsToStrings.kt create mode 100644 build-common/test/org/jetbrains/kotlin/compilerRunner/CompilerArgumentParsingTest.kt create mode 100644 build-common/test/org/jetbrains/kotlin/compilerRunner/CompilerArgumentReflectionUtils.kt create mode 100644 build-common/test/org/jetbrains/kotlin/compilerRunner/CompilerArgumentsImplementationTest.kt diff --git a/build-common/build.gradle.kts b/build-common/build.gradle.kts index 875b70fbdc0..f113a7d2a5f 100644 --- a/build-common/build.gradle.kts +++ b/build-common/build.gradle.kts @@ -29,6 +29,7 @@ dependencies { testApi(protobufFull()) testApi(kotlinStdlib()) testImplementation(commonDependency("org.jetbrains.kotlin:kotlin-reflect")) { isTransitive = false } + testImplementation("org.reflections:reflections:0.10.2") } sourceSets { diff --git a/build-common/src/org/jetbrains/kotlin/compilerRunner/ArgumentUtils.java b/build-common/src/org/jetbrains/kotlin/compilerRunner/ArgumentUtils.java index ce3749f8b6a..449470d1c04 100644 --- a/build-common/src/org/jetbrains/kotlin/compilerRunner/ArgumentUtils.java +++ b/build-common/src/org/jetbrains/kotlin/compilerRunner/ArgumentUtils.java @@ -16,25 +16,10 @@ package org.jetbrains.kotlin.compilerRunner; -import kotlin.collections.CollectionsKt; -import kotlin.jvm.JvmClassMappingKt; -import kotlin.reflect.KClass; -import kotlin.reflect.KProperty1; -import kotlin.reflect.KVisibility; -import kotlin.reflect.full.KClasses; -import kotlin.reflect.jvm.ReflectJvmMapping; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.jetbrains.kotlin.cli.common.arguments.Argument; import org.jetbrains.kotlin.cli.common.arguments.CommonToolArguments; -import org.jetbrains.kotlin.cli.common.arguments.InternalArgument; -import org.jetbrains.kotlin.cli.common.arguments.ParseCommandLineArgumentsKt; -import org.jetbrains.kotlin.idea.ExplicitDefaultSubstitutor; -import org.jetbrains.kotlin.idea.ExplicitDefaultSubstitutorsKt; -import org.jetbrains.kotlin.utils.StringsKt; import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Type; import java.util.*; public class ArgumentUtils { @@ -44,82 +29,18 @@ public class ArgumentUtils { @NotNull public static List convertArgumentsToStringList(@NotNull CommonToolArguments arguments) throws InstantiationException, IllegalAccessException, InvocationTargetException { - List convertedArguments = convertArgumentsToStringListInternal(arguments); - - Map, Collection> defaultSubstitutorsMap = - ExplicitDefaultSubstitutorsKt.getDefaultSubstitutors(); - KClass argumentsKClass = JvmClassMappingKt.getKotlinClass(arguments.getClass()); - Collection defaultSubstitutors = defaultSubstitutorsMap.get(argumentsKClass); - if (defaultSubstitutors != null) { - for (ExplicitDefaultSubstitutor substitutor : defaultSubstitutors) { - if (substitutor.isSubstitutable(convertedArguments)) convertedArguments.addAll(substitutor.getNewSubstitution()); - } - } - return convertedArguments; + return convertArgumentsToStringListInternal(arguments); } @NotNull + @Deprecated() public static List convertArgumentsToStringListNoDefaults(@NotNull CommonToolArguments arguments) throws InstantiationException, IllegalAccessException, InvocationTargetException { - return convertArgumentsToStringListInternal(arguments); + return convertArgumentsToStringList(arguments); } private static List convertArgumentsToStringListInternal(@NotNull CommonToolArguments arguments) throws InstantiationException, IllegalAccessException, InvocationTargetException { - List result = new ArrayList<>(); - Class argumentsClass = arguments.getClass(); - convertArgumentsToStringList(arguments, argumentsClass.newInstance(), JvmClassMappingKt.getKotlinClass(argumentsClass), result); - result.addAll(arguments.getFreeArgs()); - result.addAll(CollectionsKt.map(arguments.getInternalArguments(), InternalArgument::getStringRepresentation)); - return result; - } - - @SuppressWarnings("unchecked") - private static void convertArgumentsToStringList( - @NotNull CommonToolArguments arguments, - @NotNull CommonToolArguments defaultArguments, - @NotNull KClass clazz, - @NotNull List result - ) throws IllegalAccessException, InstantiationException, InvocationTargetException { - for (KProperty1 property : KClasses.getMemberProperties(clazz)) { - Argument argument = findInstance(property.getAnnotations(), Argument.class); - if (argument == null) continue; - - if (property.getVisibility() != KVisibility.PUBLIC) continue; - - Object value = property.get(arguments); - Object defaultValue = property.get(defaultArguments); - - if (value == null || Objects.equals(value, defaultValue)) continue; - - Type propertyJavaType = ReflectJvmMapping.getJavaType(property.getReturnType()); - - if (propertyJavaType instanceof Class && ((Class) propertyJavaType).isArray()) { - Object[] values = (Object[]) value; - if (values.length == 0) continue; - value = StringsKt.join(Arrays.asList(values), ","); - } - - result.add(argument.value()); - - if (propertyJavaType == boolean.class || propertyJavaType == Boolean.class) continue; - - if (ParseCommandLineArgumentsKt.isAdvanced(argument)) { - result.set(result.size() - 1, argument.value() + "=" + value.toString()); - } - else { - result.add(value.toString()); - } - } - } - - @Nullable - private static T findInstance(Iterable iterable, Class clazz) { - for (Object item : iterable) { - if (clazz.isInstance(item)) { - return clazz.cast(item); - } - } - return null; + return ArgumentsToStrings.toArgumentStrings(arguments, false); } } diff --git a/build-common/src/org/jetbrains/kotlin/compilerRunner/argumentsToStrings.kt b/build-common/src/org/jetbrains/kotlin/compilerRunner/argumentsToStrings.kt new file mode 100644 index 00000000000..b197f3b80fb --- /dev/null +++ b/build-common/src/org/jetbrains/kotlin/compilerRunner/argumentsToStrings.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +@file:JvmName("ArgumentsToStrings") + +package org.jetbrains.kotlin.compilerRunner + +import org.jetbrains.kotlin.cli.common.arguments.Argument +import org.jetbrains.kotlin.cli.common.arguments.CommonToolArguments +import org.jetbrains.kotlin.cli.common.arguments.isAdvanced +import kotlin.reflect.KClass +import kotlin.reflect.full.findAnnotation +import kotlin.reflect.full.memberProperties + +@Suppress("UNCHECKED_CAST") +@JvmOverloads +fun CommonToolArguments.toArgumentStrings(useShortNames: Boolean = false): List { + return toArgumentStrings( + this, this::class as KClass, useShortNames = useShortNames + ) +} + +@PublishedApi +internal fun toArgumentStrings(thisArguments: T, type: KClass, useShortNames: Boolean): List { + val defaultArguments = type.newArgumentsInstance() + val result = mutableListOf() + type.memberProperties.forEach { property -> + val argumentAnnotation = property.findAnnotation() ?: return@forEach + val rawPropertyValue = property.get(thisArguments) + val rawDefaultValue = property.get(defaultArguments) + + /* Default value can be omitted when not marked as 'isExplicit' */ + if (rawPropertyValue == rawDefaultValue && !argumentAnnotation.isExplicit) { + return@forEach + } + + val argumentStringValues = when { + property.returnType.classifier == Boolean::class -> listOf(rawPropertyValue?.toString() ?: false.toString()) + + (property.returnType.classifier as? KClass<*>)?.java?.isArray == true -> + getArgumentStringValue(argumentAnnotation, rawPropertyValue as Array<*>?) + + property.returnType.classifier == List::class -> + getArgumentStringValue(argumentAnnotation, (rawPropertyValue as List<*>?)?.toTypedArray()) + + else -> listOf(rawPropertyValue.toString()) + } + + val argumentName = if (useShortNames && argumentAnnotation.shortName.isNotEmpty()) argumentAnnotation.shortName + else argumentAnnotation.value + + argumentStringValues.forEach { argumentStringValue -> + + when { + /* We can just enable the flag by passing the argument name like -myFlag: Value not required */ + rawPropertyValue is Boolean && rawPropertyValue -> { + result.add(argumentName) + } + + /* Advanced (e.g. -X arguments) or boolean properties need to be passed using the '=' */ + argumentAnnotation.isAdvanced || property.returnType.classifier == Boolean::class -> { + result.add("$argumentName=$argumentStringValue") + } + else -> { + result.add(argumentName) + result.add(argumentStringValue) + } + } + } + } + + result.addAll(thisArguments.freeArgs) + result.addAll(thisArguments.internalArguments.map { it.stringRepresentation }) + return result +} + +private fun getArgumentStringValue(argumentAnnotation: Argument, values: Array<*>?): List { + if (values.isNullOrEmpty()) return emptyList() + val delimiter = argumentAnnotation.delimiter + return if (delimiter.isEmpty()) values.map { it.toString() } + else listOf(values.joinToString(delimiter)) +} + +private fun KClass.newArgumentsInstance(): T { + val argumentConstructor = constructors.find { it.parameters.isEmpty() } ?: throw IllegalArgumentException( + "$qualifiedName has no empty constructor" + ) + return argumentConstructor.call() +} \ No newline at end of file diff --git a/build-common/test/org/jetbrains/kotlin/compilerRunner/CompilerArgumentParsingTest.kt b/build-common/test/org/jetbrains/kotlin/compilerRunner/CompilerArgumentParsingTest.kt new file mode 100644 index 00000000000..7a043d8c946 --- /dev/null +++ b/build-common/test/org/jetbrains/kotlin/compilerRunner/CompilerArgumentParsingTest.kt @@ -0,0 +1,124 @@ +/* + * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.compilerRunner + +import org.jetbrains.kotlin.cli.common.arguments.* +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Named +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.Base64.getEncoder +import kotlin.random.Random +import kotlin.reflect.* +import kotlin.reflect.full.findAnnotation +import kotlin.reflect.full.isSubtypeOf +import kotlin.reflect.full.memberProperties +import kotlin.reflect.full.withNullability +import kotlin.test.assertContentEquals +import kotlin.test.fail + +class CompilerArgumentParsingTest { + + @ParameterizedTest + @MethodSource("parameters") + fun `test - parsing random compiler arguments`(type: KClass, seed: Int, useShortNames: Boolean) { + val constructor = type.constructors.find { it.parameters.isEmpty() } ?: error("Missing empty constructor on $type") + val arguments = constructor.call() + arguments.fillRandomValues(Random(seed)) + val argumentsAsStrings = arguments.toArgumentStrings(useShortNames) + val parsedArguments = parseCommandLineArguments(type, argumentsAsStrings) + assertEqualArguments(arguments, parsedArguments) + assertEquals(argumentsAsStrings, parsedArguments.toArgumentStrings(useShortNames)) + } + + companion object { + @JvmStatic + fun parameters(): List = getCompilerArgumentImplementations() + .flatMap { clazz -> + listOf(1002, 2803, 2411).flatMap { seed -> + listOf(true, false).map { useShortNames -> + Arguments.of( + Named.of("${clazz.simpleName}", clazz), + Named.of("seed: $seed", seed), + Named.of("useShortNames: $useShortNames", useShortNames) + ) + } + } + } + } +} + +private fun assertEqualArguments(expected: CommonToolArguments, actual: CommonToolArguments) { + if (expected::class != actual::class) fail("Expected class '${expected::class}', found: '${actual::class}'") + expected::class.memberProperties + .filter { it.findAnnotation() != null } + .ifEmpty { fail("No members with ${Argument::class} annotation") } + .map { property -> + @Suppress("UNCHECKED_CAST") + property as KProperty1 + val expectedValue = property.get(expected) + val actualValue = property.get(actual) + + val message = "Unexpected value in '${property.name}: '${property.returnType}'" + if (property.returnType.isSubtypeOf(typeOf?>())) { + @Suppress("UNCHECKED_CAST") + assertContentEquals( + expectedValue as Array?, actualValue as Array?, + message + ) + } else assertEquals( + expectedValue, actualValue, + message + ) + } +} + +private fun CommonToolArguments.fillRandomValues(random: Random) { + this::class.memberProperties.filterIsInstance>().forEach { property -> + @Suppress("UNCHECKED_CAST") + property as KMutableProperty1 + runCatching { + property.set(this, random.randomValue(property.returnType) ?: return@forEach) + }.getOrElse { + throw Throwable("Failed setting random value for: ${property.name}: ${property.returnType}", it) + } + } +} + +private fun Random.randomString() = nextBytes(nextInt(8, 12)).let { data -> + getEncoder().withoutPadding().encodeToString(data) +} + +private fun Random.randomBoolean() = nextBoolean() + +private fun Random.randomStringArray(): Array { + val size = nextInt(1, 5) + return Array(size) { + randomString() + } +} + +private fun Random.randomList(elementType: KType): List? { + val size = nextInt(1, 5) + return List(size) { + randomValue(elementType) ?: return null + } +} + +fun Random.randomValue(type: KType): Any? { + @Suppress("NAME_SHADOWING") + val type = type.withNullability(false) + return when { + type == typeOf() -> randomString() + type == typeOf() -> randomBoolean() + type == typeOf>() -> randomStringArray() + type.isSubtypeOf(typeOf>()) -> randomList(type.arguments.first().type ?: error("Missing elementType on $type")) + type == typeOf() -> return null + (type.classifier as? KClass<*>)?.isData == true -> null + else -> error("Unsupported type '$type'") + } +} diff --git a/build-common/test/org/jetbrains/kotlin/compilerRunner/CompilerArgumentReflectionUtils.kt b/build-common/test/org/jetbrains/kotlin/compilerRunner/CompilerArgumentReflectionUtils.kt new file mode 100644 index 00000000000..6f9cca70cfd --- /dev/null +++ b/build-common/test/org/jetbrains/kotlin/compilerRunner/CompilerArgumentReflectionUtils.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.compilerRunner + +import org.jetbrains.kotlin.cli.common.arguments.CommonToolArguments +import org.reflections.Reflections +import kotlin.reflect.KClass + +private val reflections = Reflections("org.jetbrains.kotlin") + +fun getCompilerArgumentImplementations(): List> { + return reflections.getSubTypesOf(CommonToolArguments::class.java) + .map { it.kotlin } + .filter { !it.isAbstract } + .filterNot { it.isInner } +} diff --git a/build-common/test/org/jetbrains/kotlin/compilerRunner/CompilerArgumentsImplementationTest.kt b/build-common/test/org/jetbrains/kotlin/compilerRunner/CompilerArgumentsImplementationTest.kt new file mode 100644 index 00000000000..3132e3b5fce --- /dev/null +++ b/build-common/test/org/jetbrains/kotlin/compilerRunner/CompilerArgumentsImplementationTest.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.compilerRunner + +import org.jetbrains.kotlin.cli.common.arguments.Argument +import org.jetbrains.kotlin.cli.common.arguments.CommonToolArguments +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource +import kotlin.reflect.KClass +import kotlin.reflect.KVisibility +import kotlin.reflect.full.findAnnotation +import kotlin.reflect.full.memberProperties +import kotlin.test.fail + + +class CompilerArgumentsImplementationTest { + + @ParameterizedTest + @MethodSource("implementations") + fun `test - all properties with Argument annotation - are public`(implementation: KClass) { + implementation.memberProperties.forEach { property -> + if (property.findAnnotation() != null) { + if (property.visibility != KVisibility.PUBLIC) { + fail( + "Property '${property.name}: ${property.returnType}' " + + "is marked with @${Argument::class.java.simpleName}, but is not public (${property.visibility})" + ) + } + } + } + } + + companion object { + @JvmStatic + fun implementations() = getCompilerArgumentImplementations() + } +}