CLI: Change kotlin reflection to java reflection

The command line argument parser is using between 0.25s and 0.5s
(depending on platform) on finding annotated properties. This fix
replaces the slow kotlin reflection with java reflection, which is an
order of magnitude faster.

 #KT-58183 Fixed
This commit is contained in:
Troels Bjerre Lund
2023-04-29 07:55:23 +02:00
committed by Space Team
parent b28b0e70b6
commit 111bb461a9
12 changed files with 61 additions and 43 deletions
@@ -12,8 +12,8 @@ import org.jetbrains.kotlin.cli.common.arguments.CommonToolArguments
import org.jetbrains.kotlin.cli.common.arguments.isAdvanced
import org.jetbrains.kotlin.cli.common.arguments.resolvedDelimiter
import kotlin.reflect.KClass
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.javaField
@Suppress("UNCHECKED_CAST")
@JvmOverloads
@@ -33,7 +33,7 @@ internal fun <T : CommonToolArguments> toArgumentStrings(
): List<String> = ArrayList<String>().apply {
val defaultArguments = type.newArgumentsInstance()
type.memberProperties.forEach { property ->
val argumentAnnotation = property.findAnnotation<Argument>() ?: return@forEach
val argumentAnnotation = property.javaField?.getAnnotation(Argument::class.java) ?: return@forEach
val rawPropertyValue = property.get(thisArguments)
val rawDefaultValue = property.get(defaultArguments)
@@ -12,6 +12,7 @@ import org.jetbrains.kotlin.config.JvmTarget
import kotlin.reflect.KClass
import kotlin.reflect.KProperty1
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.jvm.javaField
//used by IJ facet import
@SuppressWarnings("unused")
@@ -24,7 +25,8 @@ sealed class ExplicitDefaultSubstitutor {
abstract fun isSubstitutable(args: List<String>): Boolean
protected val argument: Argument by lazy {
substitutedProperty.findAnnotation() ?: error("Property \"${substitutedProperty.name}\" has no Argument annotation")
substitutedProperty.javaField?.getAnnotation(Argument::class.java)
?: error("Property \"${substitutedProperty.name}\" has no Argument annotation")
}
}
@@ -14,10 +14,10 @@ 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.reflect.jvm.javaField
import kotlin.test.assertContentEquals
import kotlin.test.fail
@@ -72,7 +72,7 @@ class CompilerArgumentParsingTest {
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 }
.filter { it.javaField?.getAnnotation(Argument::class.java) != null }
.ifEmpty { fail("No members with ${Argument::class} annotation") }
.map { property ->
@Suppress("UNCHECKED_CAST")
@@ -11,8 +11,8 @@ 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.reflect.jvm.javaField
import kotlin.test.fail
@@ -22,7 +22,7 @@ class CompilerArgumentsImplementationTest {
@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.javaField?.getAnnotation(Argument::class.java) != null) {
if (property.visibility != KVisibility.PUBLIC) {
fail(
"Property '${property.name}: ${property.returnType}' " +
+1
View File
@@ -32,6 +32,7 @@ dependencies {
compileOnly(toolsJarApi())
compileOnly(intellijCore())
compileOnly(commonDependency("org.jetbrains.intellij.deps:trove4j"))
compileOnly(commonDependency("org.jetbrains.kotlin:kotlin-reflect")) { isTransitive = false }
testApi(project(":compiler:backend"))
testApi(project(":compiler:cli"))
@@ -18,13 +18,13 @@ package org.jetbrains.kotlin.cli.common.arguments
import org.jetbrains.kotlin.cli.common.CompilerSystemProperties
import org.jetbrains.kotlin.konan.file.File
import org.jetbrains.kotlin.load.java.JvmAbi
import org.jetbrains.kotlin.utils.SmartList
import java.lang.reflect.Method
import kotlin.reflect.KClass
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.cast
import kotlin.reflect.full.memberProperties
@Target(AnnotationTarget.PROPERTY)
@Target(AnnotationTarget.FIELD)
annotation class Argument(
val value: String,
val shortName: String = "",
@@ -120,13 +120,22 @@ private fun <A : CommonToolArguments> parsePreprocessedCommandLineArguments(
errors: Lazy<ArgumentParseErrors>,
overrideArguments: Boolean
) {
data class ArgumentField(val property: KMutableProperty1<A, Any?>, val argument: Argument)
data class ArgumentField(val getter: Method, val setter: Method, val argument: Argument)
@Suppress("UNCHECKED_CAST")
val properties = result::class.memberProperties.mapNotNull { property ->
if (property !is KMutableProperty1<*, *>) return@mapNotNull null
val argument = property.annotations.firstOrNull { it is Argument } as Argument? ?: return@mapNotNull null
ArgumentField(property as KMutableProperty1<A, Any?>, argument)
val superClasses = mutableListOf<Class<*>>(result::class.java)
while (superClasses.last() != Any::class.java) {
superClasses.add(superClasses.last().superclass)
}
val resultClass = result::class.java
val properties = superClasses.flatMap {
it.declaredFields.mapNotNull { field ->
field.getAnnotation(Argument::class.java)?.let { argument ->
val getter = resultClass.getMethod(JvmAbi.getterName(field.name))
val setter = resultClass.getMethod(JvmAbi.setterName(field.name), field.type)
ArgumentField(getter, setter, argument)
}
}
}
val visitedArgs = mutableSetOf<String>()
@@ -144,7 +153,7 @@ private fun <A : CommonToolArguments> parsePreprocessedCommandLineArguments(
}
if (argument.value == arg) {
if (argument.isAdvanced && property.returnType.classifier != Boolean::class) {
if (argument.isAdvanced && getter.returnType.kotlin != Boolean::class) {
errors.value.extraArgumentsPassedInObsoleteForm.add(arg)
}
return true
@@ -200,9 +209,9 @@ private fun <A : CommonToolArguments> parsePreprocessedCommandLineArguments(
continue
}
val (property, argument) = argumentField
val (getter, setter, argument) = argumentField
val value: Any = when {
argumentField.property.returnType.classifier == Boolean::class -> {
getter.returnType.kotlin == Boolean::class -> {
if (arg.startsWith(argument.value + "=")) {
// Can't use toBooleanStrict yet because this part of the compiler is used in Gradle and needs API version 1.4.
when (arg.substring(argument.value.length + 1)) {
@@ -227,13 +236,12 @@ private fun <A : CommonToolArguments> parsePreprocessedCommandLineArguments(
}
}
if ((argumentField.property.returnType.classifier as? KClass<*>)?.java?.isArray == false
&& !visitedArgs.add(argument.value) && value is String && property.get(result) != value
if (!getter.returnType.isArray && !visitedArgs.add(argument.value) && value is String && getter(result) != value
) {
errors.value.duplicateArguments[argument.value] = value
}
updateField(property, result, value, argument.resolvedDelimiter, overrideArguments)
updateField(getter, setter, result, value, argument.resolvedDelimiter, overrideArguments)
}
result.freeArgs += freeArgs
@@ -257,25 +265,27 @@ private fun <A : CommonToolArguments> A.updateInternalArguments(
}
private fun <A : CommonToolArguments> updateField(
property: KMutableProperty1<A, Any?>,
getter: Method,
setter: Method,
result: A,
value: Any,
delimiter: String?,
overrideArguments: Boolean
) {
when (property.returnType.classifier) {
Boolean::class, String::class -> property.set(result, value)
when (getter.returnType.kotlin) {
Boolean::class, String::class -> setter(result, value)
Array<String>::class -> {
val newElements = if (delimiter.isNullOrEmpty()) {
arrayOf(value as String)
} else {
(value as String).split(delimiter).toTypedArray()
}
@Suppress("UNCHECKED_CAST")
val oldValue = property.get(result) as Array<String>?
property.set(result, if (oldValue != null && !overrideArguments) arrayOf(*oldValue, *newElements) else newElements)
val oldValue = getter(result) as Array<String>?
setter(result, if (oldValue != null && !overrideArguments) arrayOf(*oldValue, *newElements) else newElements)
}
else -> throw IllegalStateException("Unsupported argument type: ${property.returnType}")
else -> throw IllegalStateException("Unsupported argument type: ${getter.returnType}")
}
}
@@ -17,11 +17,11 @@
package org.jetbrains.kotlin.cli.common;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.util.containers.ContainerUtil;
import kotlin.jvm.JvmClassMappingKt;
import kotlin.reflect.KCallable;
import kotlin.reflect.KClass;
import kotlin.reflect.KProperty1;
import kotlin.reflect.jvm.ReflectJvmMapping;
import kotlin.text.StringsKt;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.kotlin.cli.common.arguments.Argument;
@@ -29,6 +29,8 @@ import org.jetbrains.kotlin.cli.common.arguments.CommonToolArguments;
import org.jetbrains.kotlin.cli.common.arguments.ParseCommandLineArgumentsKt;
import org.jetbrains.kotlin.cli.common.arguments.PreprocessCommandLineArgumentsKt;
import java.lang.reflect.Field;
public class Usage {
public static final String BAT_DELIMITER_CHARACTERS_NOTE =
"Note: on Windows, arguments that contain delimiter characters (whitespace, =, ;, ,) need to be surrounded with double quotes (\").";
@@ -71,7 +73,8 @@ public class Usage {
}
private static void propertyUsage(@NotNull StringBuilder sb, @NotNull KProperty1<?, ?> property, boolean extraHelp) {
Argument argument = ContainerUtil.findInstance(property.getAnnotations(), Argument.class);
Field field = ReflectJvmMapping.getJavaField(property);
Argument argument = field.getAnnotation(Argument.class);
if (argument == null) return;
if (extraHelp != ParseCommandLineArgumentsKt.isAdvanced(argument)) return;
@@ -16,17 +16,18 @@ import kotlin.reflect.KClass
import kotlin.reflect.KProperty1
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.withNullability
import kotlin.reflect.jvm.javaField
// Additional properties that should be included in interface
// Additional properties that should be included
@Suppress("unused")
interface AdditionalGradleProperties {
private class AdditionalGradleProperties {
@GradleOption(
value = DefaultValue.EMPTY_STRING_LIST_DEFAULT,
gradleInputType = GradleInputTypes.INPUT,
shouldGenerateDeprecatedKotlinOptions = true,
)
@Argument(value = "", description = "A list of additional compiler arguments")
var freeCompilerArgs: List<String>
var freeCompilerArgs = listOf<String>()
}
private data class GeneratedOptions(
@@ -869,7 +870,7 @@ private fun Printer.generateOptionDeprecation(property: KProperty1<*, *>) {
}
private fun Printer.generateDoc(property: KProperty1<*, *>) {
val description = property.findAnnotation<Argument>()!!.description
val description = property.javaField!!.getAnnotation(Argument::class.java).description
val possibleValues = property.gradleValues.possibleValues
val defaultValue = property.gradleValues.defaultValue
@@ -896,7 +897,7 @@ private fun generateMarkdown(properties: List<KProperty1<*, *>>) {
if (name == "includeRuntime") continue // This option has no effect in Gradle builds
val renderName = listOfNotNull("`$name`", property.findAnnotation<GradleDeprecatedOption>()?.let { "__(Deprecated)__" })
.joinToString(" ")
val description = property.findAnnotation<Argument>()!!.description
val description = property.javaField!!.getAnnotation(Argument::class.java).description
val possibleValues = property.gradleValues.possibleValues
val defaultValue = when (property.gradleDefaultValue) {
"null" -> ""
@@ -9,6 +9,7 @@ import kotlin.reflect.KClass
import kotlin.reflect.KProperty1
import kotlin.reflect.KType
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.javaField
object CompilerArgumentsContentProspector {
private val argumentPropertiesCache: MutableMap<KClass<out CommonToolArguments>, Collection<KProperty1<out CommonToolArguments, *>>> =
@@ -24,7 +25,7 @@ object CompilerArgumentsContentProspector {
mutableMapOf()
private fun getCompilerArgumentsProperties(kClass: KClass<out CommonToolArguments>) = argumentPropertiesCache.getOrPut(kClass) {
kClass.memberProperties.filter { prop -> prop.annotations.any { it is Argument } }
kClass.memberProperties.filter { prop -> prop.javaField?.getAnnotation(Argument::class.java) != null }
}
private inline fun <reified R : Any?> Collection<KProperty1<out CommonToolArguments, *>>.filterByReturnType(predicate: (KType?) -> Boolean) =
@@ -15,7 +15,7 @@ import org.jetbrains.kotlin.platform.jvm.JvmPlatforms
import org.jetbrains.kotlin.utils.DescriptionAware
import org.jetbrains.kotlin.utils.addToStdlib.safeAs
import kotlin.reflect.KProperty1
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.jvm.javaField
@Deprecated("Use IdePlatformKind instead.", level = DeprecationLevel.ERROR)
sealed class TargetPlatformKind<out Version : TargetPlatformVersion>(
@@ -206,7 +206,7 @@ class KotlinFacetSettings {
val isEnabledByCompilerArgument = compilerArguments?.safeAs<A>()?.let(settingReference::get)
if (isEnabledByCompilerArgument == true) return true
val isEnabledByAdditionalSettings = run {
val stringArgumentName = settingReference.findAnnotation<Argument>()?.value ?: return@run null
val stringArgumentName = settingReference.javaField?.getAnnotation(Argument::class.java)?.value ?: return@run null
compilerSettings?.additionalArguments?.contains(stringArgumentName, ignoreCase = true)
}
return isEnabledByAdditionalSettings ?: false
@@ -28,7 +28,7 @@ import org.jetbrains.kotlin.gradle.util.assertNotNull
import org.jetbrains.kotlin.gradle.util.buildProjectWithJvm
import org.jetbrains.kotlin.gradle.util.buildProjectWithMPP
import org.jetbrains.kotlin.gradle.util.main
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.jvm.javaField
import kotlin.test.*
@@ -78,7 +78,7 @@ class KotlinCompileArgumentsTest {
val arguments = mainCompilationTask.createCompilerArguments(lenient)
val argumentsString = ArgumentUtils.convertArgumentsToStringList(arguments)
val jvmTargetArgument = K2JVMCompilerArguments::jvmTarget.findAnnotation<Argument>()!!.value
val jvmTargetArgument = K2JVMCompilerArguments::jvmTarget.javaField!!.getAnnotation(Argument::class.java)!!.value
if (jvmTargetArgument !in argumentsString) fail("Missing '$jvmTargetArgument' in argument list")
val indexOfJvmTargetArgument = argumentsString.indexOf(jvmTargetArgument)
val jvmTargetTargetArgumentValue = argumentsString.getOrNull(indexOfJvmTargetArgument + 1)
@@ -158,4 +158,4 @@ class KotlinCompileArgumentsTest {
compileTask.createCompilerArguments(lenient).fragmentSources.orEmpty().toSet()
)
}
}
}
@@ -17,8 +17,8 @@ import org.jetbrains.kotlin.diagnostics.impl.BaseDiagnosticsCollector
import org.jetbrains.kotlin.diagnostics.rendering.RootDiagnosticRendererFactory
import org.jetbrains.kotlin.psi
import org.jetbrains.kotlin.scripting.definitions.MessageReporter
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.jvm.javaField
import kotlin.script.experimental.api.ResultWithDiagnostics
import kotlin.script.experimental.api.ScriptDiagnostic
import kotlin.script.experimental.api.SourceCode
@@ -194,7 +194,7 @@ private fun reportInvalidArguments(
): Boolean {
val invalidArgKeys = toIgnore.mapNotNull { argProperty ->
if (argProperty.get(arguments) != argProperty.get(reportingState.currentArguments)) {
argProperty.annotations.firstIsInstanceOrNull<Argument>()?.value
argProperty.javaField?.getAnnotation(Argument::class.java)?.value
?: throw IllegalStateException("unknown compiler argument property: $argProperty: no Argument annotation found")
} else null
}