[CLI] Implement CommonToolArguments.toStringList (to replace convertArgumentsToStringList`)

KTIJ-24976
This commit is contained in:
Sebastian Sellmair
2023-03-22 16:59:25 +01:00
committed by Space Team
parent 2b893365aa
commit d07b1b6502
6 changed files with 279 additions and 83 deletions
+1
View File
@@ -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 {
@@ -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<String> convertArgumentsToStringList(@NotNull CommonToolArguments arguments)
throws InstantiationException, IllegalAccessException, InvocationTargetException {
List<String> convertedArguments = convertArgumentsToStringListInternal(arguments);
Map<KClass<? extends CommonToolArguments>, Collection<ExplicitDefaultSubstitutor>> defaultSubstitutorsMap =
ExplicitDefaultSubstitutorsKt.getDefaultSubstitutors();
KClass<? extends CommonToolArguments> argumentsKClass = JvmClassMappingKt.getKotlinClass(arguments.getClass());
Collection<ExplicitDefaultSubstitutor> 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<String> convertArgumentsToStringListNoDefaults(@NotNull CommonToolArguments arguments)
throws InstantiationException, IllegalAccessException, InvocationTargetException {
return convertArgumentsToStringListInternal(arguments);
return convertArgumentsToStringList(arguments);
}
private static List<String> convertArgumentsToStringListInternal(@NotNull CommonToolArguments arguments)
throws InstantiationException, IllegalAccessException, InvocationTargetException {
List<String> result = new ArrayList<>();
Class<? extends CommonToolArguments> 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<String> 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> T findInstance(Iterable<? super T> iterable, Class<T> clazz) {
for (Object item : iterable) {
if (clazz.isInstance(item)) {
return clazz.cast(item);
}
}
return null;
return ArgumentsToStrings.toArgumentStrings(arguments, false);
}
}
@@ -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<String> {
return toArgumentStrings(
this, this::class as KClass<CommonToolArguments>, useShortNames = useShortNames
)
}
@PublishedApi
internal fun <T : CommonToolArguments> toArgumentStrings(thisArguments: T, type: KClass<T>, useShortNames: Boolean): List<String> {
val defaultArguments = type.newArgumentsInstance()
val result = mutableListOf<String>()
type.memberProperties.forEach { property ->
val argumentAnnotation = property.findAnnotation<Argument>() ?: 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<String> {
if (values.isNullOrEmpty()) return emptyList()
val delimiter = argumentAnnotation.delimiter
return if (delimiter.isEmpty()) values.map { it.toString() }
else listOf(values.joinToString(delimiter))
}
private fun <T : CommonToolArguments> KClass<T>.newArgumentsInstance(): T {
val argumentConstructor = constructors.find { it.parameters.isEmpty() } ?: throw IllegalArgumentException(
"$qualifiedName has no empty constructor"
)
return argumentConstructor.call()
}
@@ -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<out CommonToolArguments>, 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<Arguments> = 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<Argument>() != null }
.ifEmpty { fail("No members with ${Argument::class} annotation") }
.map { property ->
@Suppress("UNCHECKED_CAST")
property as KProperty1<Any, Any?>
val expectedValue = property.get(expected)
val actualValue = property.get(actual)
val message = "Unexpected value in '${property.name}: '${property.returnType}'"
if (property.returnType.isSubtypeOf(typeOf<Array<*>?>())) {
@Suppress("UNCHECKED_CAST")
assertContentEquals(
expectedValue as Array<Any?>?, actualValue as Array<Any?>?,
message
)
} else assertEquals(
expectedValue, actualValue,
message
)
}
}
private fun CommonToolArguments.fillRandomValues(random: Random) {
this::class.memberProperties.filterIsInstance<KMutableProperty1<*, *>>().forEach { property ->
@Suppress("UNCHECKED_CAST")
property as KMutableProperty1<Any, Any>
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<String> {
val size = nextInt(1, 5)
return Array(size) {
randomString()
}
}
private fun Random.randomList(elementType: KType): List<Any>? {
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<String>() -> randomString()
type == typeOf<Boolean>() -> randomBoolean()
type == typeOf<Array<String>>() -> randomStringArray()
type.isSubtypeOf(typeOf<List<*>>()) -> randomList(type.arguments.first().type ?: error("Missing elementType on $type"))
type == typeOf<InternalArgument>() -> return null
(type.classifier as? KClass<*>)?.isData == true -> null
else -> error("Unsupported type '$type'")
}
}
@@ -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<KClass<out CommonToolArguments>> {
return reflections.getSubTypesOf(CommonToolArguments::class.java)
.map { it.kotlin }
.filter { !it.isAbstract }
.filterNot { it.isInner }
}
@@ -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<out CommonToolArguments>) {
implementation.memberProperties.forEach { property ->
if (property.findAnnotation<Argument>() != 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()
}
}