All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jetbrains.kotlin.cli.common.arguments.parseCommandLineArguments.kt Maven / Gradle / Ivy

There is a newer version: 2.1.0-RC
Show newest version
/*
 * Copyright 2010-2017 JetBrains s.r.o.
 *
 * 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
 *
 * http://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 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 java.util.concurrent.ConcurrentHashMap
import kotlin.reflect.KClass
import kotlin.reflect.cast

@Target(AnnotationTarget.FIELD)
annotation class Argument(
    val value: String,
    val shortName: String = "",
    val deprecatedName: String = "",
    @property:RawDelimiter
    val delimiter: String = Delimiters.default,
    val valueDescription: String = "",
    val description: String
) {
    @RequiresOptIn(
        message = "The raw delimiter value needs to be resolved. See 'resolvedDelimiter'. Using the raw value requires opt-in",
        level = RequiresOptIn.Level.ERROR
    )
    annotation class RawDelimiter

    object Delimiters {
        const val default = ","
        const val none = ""
        const val pathSeparator = ""
    }
}

val Argument.isAdvanced: Boolean
    get() = value.startsWith(ADVANCED_ARGUMENT_PREFIX) && value.length > ADVANCED_ARGUMENT_PREFIX.length

@OptIn(Argument.RawDelimiter::class)
val Argument.resolvedDelimiter: String?
    get() = when (delimiter) {
        Argument.Delimiters.none -> null
        Argument.Delimiters.pathSeparator -> File.pathSeparator
        else -> delimiter
    }

private const val ADVANCED_ARGUMENT_PREFIX = "-X"
private const val FREE_ARGS_DELIMITER = "--"

data class ArgumentParseErrors(
    val unknownArgs: MutableList = SmartList(),

    val unknownExtraFlags: MutableList = SmartList(),

    // Names of extra (-X...) arguments which have been passed in an obsolete form ("-Xaaa bbb", instead of "-Xaaa=bbb")
    val extraArgumentsPassedInObsoleteForm: MutableList = SmartList(),

    // Non-boolean arguments which have been passed multiple times, possibly with different values.
    // The key in the map is the name of the argument, the value is the last passed value.
    val duplicateArguments: MutableMap = mutableMapOf(),

    // Arguments where [Argument.deprecatedName] was used; the key is the deprecated name, the value is the new name ([Argument.value])
    val deprecatedArguments: MutableMap = mutableMapOf(),

    var argumentWithoutValue: String? = null,

    var booleanArgumentWithValue: String? = null,

    val argfileErrors: MutableList = SmartList(),

    // Reports from internal arguments parsers
    val internalArgumentsParsingProblems: MutableList = SmartList()
)

inline fun  parseCommandLineArguments(args: List): T {
    return parseCommandLineArguments(T::class, args)
}

fun  parseCommandLineArguments(clazz: KClass, args: List): T {
    val constructor = clazz.java.constructors.find { it.parameters.isEmpty() }
        ?: error("Missing empty constructor on '${clazz.java.name}")
    val arguments = clazz.cast(constructor.newInstance())
    parseCommandLineArguments(args, arguments)
    return arguments
}


// Parses arguments into the passed [result] object. Errors related to the parsing will be collected into [CommonToolArguments.errors].
fun  parseCommandLineArguments(args: List, result: A, overrideArguments: Boolean = false) {
    val errors = lazy { result.errors ?: ArgumentParseErrors().also { result.errors = it } }
    val preprocessed = preprocessCommandLineArguments(args, errors)
    parsePreprocessedCommandLineArguments(preprocessed, result, errors, overrideArguments)
}

fun  parseCommandLineArgumentsFromEnvironment(arguments: A) {
    val settingsFromEnvironment = CompilerSystemProperties.LANGUAGE_VERSION_SETTINGS.value?.takeIf { it.isNotEmpty() }
        ?.split(Regex("""\s"""))
        ?.filterNot { it.isBlank() }
        ?: return
    parseCommandLineArguments(settingsFromEnvironment, arguments, overrideArguments = true)
}

private data class ArgumentField(val getter: Method, val setter: Method, val argument: Argument)

private val argumentsCache = ConcurrentHashMap, Map>()

private fun getArguments(klass: Class<*>): Map = argumentsCache.getOrPut(klass) {
    if (klass == Any::class.java) emptyMap()
    else buildMap {
        putAll(getArguments(klass.superclass))
        for (field in klass.declaredFields) {
            field.getAnnotation(Argument::class.java)?.let { argument ->
                val getter = klass.getMethod(JvmAbi.getterName(field.name))
                val setter = klass.getMethod(JvmAbi.setterName(field.name), field.type)
                val argumentField = ArgumentField(getter, setter, argument)
                for (key in listOf(argument.value, argument.shortName, argument.deprecatedName)) {
                    if (key.isNotEmpty()) put(key, argumentField)
                }
            }
        }
    }
}

private fun  parsePreprocessedCommandLineArguments(
    args: List,
    result: A,
    errors: Lazy,
    overrideArguments: Boolean
) {
    val properties = getArguments(result::class.java)

    val visitedArgs = mutableSetOf()
    var freeArgsStarted = false

    val freeArgs = ArrayList()
    val internalArguments = ArrayList()

    var i = 0
    loop@ while (i < args.size) {
        val arg = args[i++]

        if (freeArgsStarted) {
            freeArgs.add(arg)
            continue
        }
        if (arg == FREE_ARGS_DELIMITER) {
            freeArgsStarted = true
            continue
        }

        if (arg.startsWith(InternalArgumentParser.INTERNAL_ARGUMENT_PREFIX)) {
            val matchingParsers = InternalArgumentParser.PARSERS.filter { it.canParse(arg) }
            assert(matchingParsers.size <= 1) { "Internal error: internal argument $arg can be ambiguously parsed by parsers ${matchingParsers.joinToString()}" }

            val parser = matchingParsers.firstOrNull()

            if (parser == null) {
                errors.value.unknownExtraFlags += arg
            } else {
                val newInternalArgument = parser.parseInternalArgument(arg, errors.value) ?: continue
                // Manual language feature setting overrides the previous value of the same feature setting, if it exists.
                internalArguments.removeIf {
                    (it as? ManualLanguageFeatureSetting)?.languageFeature ==
                            (newInternalArgument as? ManualLanguageFeatureSetting)?.languageFeature
                }
                internalArguments.add(newInternalArgument)
            }

            continue
        }

        val key = arg.substringBefore('=')
        val argumentField = properties[key]
        if (argumentField == null) {
            when {
                arg.startsWith(ADVANCED_ARGUMENT_PREFIX) -> errors.value.unknownExtraFlags.add(arg)
                arg.startsWith("-") -> errors.value.unknownArgs.add(arg)
                else -> freeArgs.add(arg)
            }
            continue
        }

        val (getter, setter, argument) = argumentField

        // Tests for -shortName=value, which isn't currently allowed.
        if (key != arg && key == argument.shortName) {
            errors.value.unknownArgs.add(arg)
            continue
        }

        val deprecatedName = argument.deprecatedName
        if (deprecatedName == key) {
            errors.value.deprecatedArguments[deprecatedName] = argument.value
        }

        if (argument.value == arg) {
            if (argument.isAdvanced && getter.returnType.kotlin != Boolean::class) {
                errors.value.extraArgumentsPassedInObsoleteForm.add(arg)
            }
        }

        val value: Any = when {
            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)) {
                        "true" -> true
                        "false" -> false
                        else -> true.also { errors.value.booleanArgumentWithValue = arg }
                    }
                } else true
            }
            arg.startsWith(argument.value + "=") -> {
                arg.substring(argument.value.length + 1)
            }
            arg.startsWith(argument.deprecatedName + "=") -> {
                arg.substring(argument.deprecatedName.length + 1)
            }
            i == args.size -> {
                errors.value.argumentWithoutValue = arg
                break@loop
            }
            else -> {
                args[i++]
            }
        }

        if (!getter.returnType.isArray && !visitedArgs.add(argument.value) && value is String && getter(result) != value
        ) {
            errors.value.duplicateArguments[argument.value] = value
        }

        updateField(getter, setter, result, value, argument.resolvedDelimiter, overrideArguments)
    }

    result.freeArgs += freeArgs
    result.updateInternalArguments(internalArguments, overrideArguments)
}

private fun  A.updateInternalArguments(
    newInternalArguments: ArrayList,
    overrideArguments: Boolean
) {
    val filteredExistingArguments = if (overrideArguments) {
        internalArguments.filter { existingArgument ->
            existingArgument !is ManualLanguageFeatureSetting ||
                    newInternalArguments.none {
                        it is ManualLanguageFeatureSetting && it.languageFeature == existingArgument.languageFeature
                    }
        }
    } else internalArguments

    internalArguments = filteredExistingArguments + newInternalArguments
}

private fun  updateField(
    getter: Method,
    setter: Method,
    result: A,
    value: Any,
    delimiter: String?,
    overrideArguments: Boolean
) {
    when (getter.returnType.kotlin) {
        Boolean::class, String::class -> setter(result, value)
        Array::class -> {
            val newElements = if (delimiter.isNullOrEmpty()) {
                arrayOf(value as String)
            } else {
                (value as String).split(delimiter).toTypedArray()
            }

            @Suppress("UNCHECKED_CAST")
            val oldValue = getter(result) as Array?
            setter(result, if (oldValue != null && !overrideArguments) arrayOf(*oldValue, *newElements) else newElements)
        }
        else -> throw IllegalStateException("Unsupported argument type: ${getter.returnType}")
    }
}

/**
 * @return error message if arguments are parsed incorrectly, null otherwise
 */
fun validateArguments(errors: ArgumentParseErrors?): String? {
    if (errors == null) return null
    if (errors.argumentWithoutValue != null) {
        return "No value passed for argument ${errors.argumentWithoutValue}"
    }
    errors.booleanArgumentWithValue?.let { arg ->
        return "No value expected for boolean argument ${arg.substringBefore('=')}. Please remove the value: $arg"
    }
    if (errors.unknownArgs.isNotEmpty()) {
        return "Invalid argument: ${errors.unknownArgs.first()}"
    }
    return null
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy