Skip to content

Commit

Permalink
Introduce DefaultValue type
Browse files Browse the repository at this point in the history
  • Loading branch information
Markus Schwarz committed Jul 1, 2021
1 parent f6d75dd commit 8eabb07
Show file tree
Hide file tree
Showing 13 changed files with 179 additions and 129 deletions.
Expand Up @@ -3,31 +3,13 @@ package io.gitlab.arturbosch.detekt.generator.collection
data class Configuration(
val name: String,
val description: String,
val defaultValue: String,
val defaultAndroidValue: String?,
val defaultValue: DefaultValue,
val defaultAndroidValue: DefaultValue?,
val deprecated: String?
) {
fun isDeprecated() = deprecated != null

fun isDefaultValueNonEmptyList() = defaultValue.isNonEmptyList()

fun getDefaultValueAsList(): List<String> {
if (defaultValue.isNonEmptyList()) {
return defaultValue.toList()
}
error("default value '$defaultValue' is not a list")
}

private fun String.isNonEmptyList(): Boolean = NON_EMPTY_LIST_REGEX.matchEntire(this) != null

private fun String.toList(): List<String> =
trim()
.removePrefix("[")
.removeSuffix("]")
.split(",")
.map { it.trim().removeSurrounding("'") }

companion object {
private val NON_EMPTY_LIST_REGEX = Regex("""\[.*[\S]+.*]""")
}
fun getDefaultValueAsList(): List<String> = defaultValue.getAsList()
}
Expand Up @@ -6,6 +6,10 @@ import io.gitlab.arturbosch.detekt.generator.collection.ConfigurationCollector.C
import io.gitlab.arturbosch.detekt.generator.collection.ConfigurationCollector.ConfigWithFallbackSupport.FALLBACK_DELEGATE_NAME
import io.gitlab.arturbosch.detekt.generator.collection.ConfigurationCollector.ConfigWithFallbackSupport.checkUsingInvalidFallbackReference
import io.gitlab.arturbosch.detekt.generator.collection.ConfigurationCollector.ConfigWithFallbackSupport.isFallbackConfigDelegate
import io.gitlab.arturbosch.detekt.generator.collection.ConfigurationCollector.DefaultValueSupport.getAndroidDefaultValue
import io.gitlab.arturbosch.detekt.generator.collection.ConfigurationCollector.DefaultValueSupport.getDefaultValue
import io.gitlab.arturbosch.detekt.generator.collection.ConfigurationCollector.DefaultValueSupport.toDefaultValueIfLiteral
import io.gitlab.arturbosch.detekt.generator.collection.DefaultValue.Companion.of
import io.gitlab.arturbosch.detekt.generator.collection.exception.InvalidDocumentationException
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtConstantExpression
Expand All @@ -23,7 +27,7 @@ import io.gitlab.arturbosch.detekt.api.internal.Configuration as ConfigAnnotatio

class ConfigurationCollector {

private val constantsByName = mutableMapOf<String, String>()
private val constantsByName = mutableMapOf<String, DefaultValue>()
private val properties = mutableListOf<KtProperty>()

fun getConfiguration(): List<Configuration> {
Expand All @@ -42,25 +46,22 @@ class ConfigurationCollector {
)
}

private fun resolveConstantOrNull(prop: KtProperty): Pair<String, String>? {
private fun resolveConstantOrNull(prop: KtProperty): Pair<String, DefaultValue>? {
if (prop.isVar) return null

val propertyName = checkNotNull(prop.name)
val constantOrNull = prop.getConstantValueAsStringOrNull()
val constantOrNull = prop.getConstantValue()

return constantOrNull?.let { propertyName to it }
}

private fun KtProperty.getConstantValueAsStringOrNull(): String? {
private fun KtProperty.getConstantValue(): DefaultValue? {
if (hasListDeclaration()) {
return getListDeclaration()
.valueArguments
.map { "'${it.text.withoutQuotes()}'" }
.toString()
return of(getListDeclaration().valueArguments.map { it.text.withoutQuotes() })
}

return findDescendantOfType<KtConstantExpression>()?.text
?: findDescendantOfType<KtStringTemplateExpression>()?.text?.withoutQuotes()
return findDescendantOfType<KtConstantExpression>()?.toDefaultValueIfLiteral()
?: findDescendantOfType<KtStringTemplateExpression>()?.toDefaultValueIfLiteral()
}

private fun KtProperty.parseConfigurationAnnotation(): Configuration? = when {
Expand All @@ -83,59 +84,61 @@ class ConfigurationCollector {
val propertyName: String = checkNotNull(name)
val deprecationMessage = firstAnnotationParameterOrNull(Deprecated::class)
val description: String = firstAnnotationParameter(ConfigAnnotation::class)
val defaultValueAsString = getDefaultValueAsString()
val defaultAndroidValueAsString = getDefaultAndroidValueAsString()
val defaultValue = getDefaultValue(constantsByName)
val defaultAndroidValue = getAndroidDefaultValue(constantsByName)

return Configuration(
name = propertyName,
description = description,
defaultValue = defaultValueAsString,
defaultAndroidValue = defaultAndroidValueAsString,
defaultValue = defaultValue,
defaultAndroidValue = defaultAndroidValue,
deprecated = deprecationMessage,
)
}

private fun KtProperty.getDefaultValueAsString(): String {
val defaultValueArgument = getValueArgument(
name = DEFAULT_VALUE_ARGUMENT_NAME,
actionForPositionalMatch = { arguments ->
when {
isFallbackConfigDelegate() -> arguments[1]
isAndroidVariantConfigDelegate() -> arguments[0]
else -> arguments[0]
private object DefaultValueSupport {
fun KtProperty.getDefaultValue(constantsByName: Map<String, DefaultValue>): DefaultValue {
val defaultValueArgument = getValueArgument(
name = DEFAULT_VALUE_ARGUMENT_NAME,
actionForPositionalMatch = { arguments ->
when {
isFallbackConfigDelegate() -> arguments[1]
isAndroidVariantConfigDelegate() -> arguments[0]
else -> arguments[0]
}
}
}
) ?: invalidDocumentation { "'$name' is not a delegated property" }
return formatDefaultValueExpression(checkNotNull(defaultValueArgument.getArgumentExpression()))
}
) ?: invalidDocumentation { "'$name' is not a delegated property" }
return checkNotNull(defaultValueArgument.getArgumentExpression()).toDefaultValue(constantsByName)
}

fun KtProperty.getAndroidDefaultValue(constantsByName: Map<String, DefaultValue>): DefaultValue? {
val defaultValueArgument = getValueArgument(
name = DEFAULT_ANDROID_VALUE_ARGUMENT_NAME,
actionForPositionalMatch = { arguments ->
when {
isAndroidVariantConfigDelegate() -> arguments[1]
else -> null
}
}
)
return defaultValueArgument?.getArgumentExpression()?.toDefaultValue(constantsByName)
}

private fun KtProperty.getDefaultAndroidValueAsString(): String? {
val defaultValueArgument = getValueArgument(
name = DEFAULT_ANDROID_VALUE_ARGUMENT_NAME,
actionForPositionalMatch = { arguments ->
when {
isAndroidVariantConfigDelegate() -> arguments[1]
else -> null
fun KtExpression.toDefaultValue(constantsByName: Map<String, DefaultValue>): DefaultValue {
val listDeclarationForDefault = getListDeclarationOrNull()
if (listDeclarationForDefault != null) {
val listValues = listDeclarationForDefault.valueArguments.map {
(constantsByName[it.text]?.getAsPlainString() ?: it.text.withoutQuotes())
}
return of(listValues)
}
)
val defaultValueExpression = defaultValueArgument?.getArgumentExpression() ?: return null
return formatDefaultValueExpression(defaultValueExpression)
}

private fun KtProperty.formatDefaultValueExpression(ktExpression: KtExpression): String {
val listDeclarationForDefault = ktExpression.getListDeclarationOrNull()
if (listDeclarationForDefault != null) {
return listDeclarationForDefault.valueArguments.map {
val value = constantsByName[it.text] ?: it.text
"'${value.withoutQuotes()}'"
}.toString()
return toDefaultValueIfLiteral()
?: constantsByName[text.withoutQuotes()]
?: error("$text is neither a literal nor a constant")
}

val defaultValueOrConstantName = checkNotNull(ktExpression.text.withoutQuotes())
val defaultValue = constantsByName[defaultValueOrConstantName] ?: defaultValueOrConstantName
val needsQuotes = declaredTypeOrNull in TYPES_THAT_NEED_QUOTATION_FOR_DEFAULT
return if (needsQuotes) "'$defaultValue'" else defaultValue
fun KtExpression.toDefaultValueIfLiteral(): DefaultValue? = createDefaultValueIfLiteral(text)
}

private object ConfigWithFallbackSupport {
Expand Down Expand Up @@ -181,14 +184,6 @@ class ConfigurationCollector {
private const val EMPTY_LIST = "emptyList"
private val LIST_CREATORS = setOf(LIST_OF, EMPTY_LIST)

private const val TYPE_STRING = "String"
private const val TYPE_REGEX = "Regex"
private const val TYPE_SPLIT_PATTERN = "SplitPattern"
private val TYPES_THAT_NEED_QUOTATION_FOR_DEFAULT = listOf(TYPE_STRING, TYPE_REGEX, TYPE_SPLIT_PATTERN)

private val KtProperty.declaredTypeOrNull: String?
get() = typeReference?.text

private fun KtElement.getListDeclaration(): KtCallExpression =
checkNotNull(getListDeclarationOrNull())

Expand Down
@@ -0,0 +1,38 @@
package io.gitlab.arturbosch.detekt.generator.collection

sealed interface DefaultValue {
fun isNonEmptyList(): Boolean = false
fun getAsList(): List<String> = error("default value is not a list")
fun getAsPlainString(): String
fun getQuotedIfNecessary(): String = getAsPlainString()

companion object {
fun of(defaultValue: String): DefaultValue = StringDefault(defaultValue)
fun of(defaultValue: Boolean): DefaultValue = BooleanDefault(defaultValue)
fun of(defaultValue: Int): DefaultValue = IntegerDefault(defaultValue)
fun of(defaultValue: List<String>): DefaultValue = ListDefault(defaultValue)
}
}

private data class StringDefault(private val defaultValue: String) : DefaultValue {
private val quoted = "'$defaultValue'"
override fun getAsPlainString(): String = defaultValue
override fun getQuotedIfNecessary(): String = quoted
}

private data class BooleanDefault(private val defaultValue: Boolean) : DefaultValue {
override fun getAsPlainString(): String = defaultValue.toString()
}

private data class IntegerDefault(private val defaultValue: Int) : DefaultValue {
override fun getAsPlainString(): String = defaultValue.toString()
}

private data class ListDefault(private val defaultValue: List<String>) : DefaultValue {
private val quoted: String = defaultValue.map { "'$it'" }.toString()

override fun isNonEmptyList(): Boolean = defaultValue.isNotEmpty()
override fun getAsList(): List<String> = defaultValue.ifEmpty { error("default value is an empty list") }
override fun getAsPlainString(): String = defaultValue.toString()
override fun getQuotedIfNecessary(): String = quoted
}
@@ -0,0 +1,22 @@
package io.gitlab.arturbosch.detekt.generator.collection

import org.jetbrains.kotlin.lexer.KtTokens.FALSE_KEYWORD
import org.jetbrains.kotlin.lexer.KtTokens.TRUE_KEYWORD

fun createDefaultValueIfLiteral(maybeLiteral: String): DefaultValue? = maybeLiteral.toDefaultValueIfLiteral()

private fun String.toDefaultValueIfLiteral(): DefaultValue? {
return when {
isStringLiteral() -> DefaultValue.of(withoutQuotes())
isBooleanLiteral() -> DefaultValue.of(toBoolean())
isIntegerLiteral() -> DefaultValue.of(withoutUnderscores().toInt())
else -> null
}
}

private fun String.withoutUnderscores() = replace("_", "")
private fun String.isStringLiteral() = length > 1 && startsWith(QUOTES) && endsWith(QUOTES)
private fun String.isBooleanLiteral() = this == TRUE_KEYWORD.value || this == FALSE_KEYWORD.value
private fun String.isIntegerLiteral() = withoutUnderscores().toIntOrNull() != null

private const val QUOTES = "\""
Expand Up @@ -21,10 +21,20 @@ private fun KDocTag.parseConfigTag(): Configuration {
val content: String = getContent()
val delimiterIndex = content.indexOf('-')
val name = content.substring(0, delimiterIndex - 1)
val defaultValue = configurationDefaultValueRegex.find(content)
val rawDefaultValue = configurationDefaultValueRegex.find(content)
?.groupValues
?.get(1)
?.trim() ?: ""
val defaultValue = if (rawDefaultValue.startsWith("-")) {
DefaultValue.of(rawDefaultValue.lines().map { it.trim().removePrefix("- ") })
} else {
val unquoted = rawDefaultValue.replace('\'', '"')
createDefaultValueIfLiteral(unquoted) ?: throw InvalidDocumentationException(
"[${containingFile.name}] '$content' doesn't seem to contain a correctly formatted default value.\n" +
EXPECTED_CONFIGURATION_FORMAT
)
}

val deprecatedMessage = configurationDeprecatedRegex.find(content)
?.groupValues
?.get(1)
Expand Down
Expand Up @@ -65,7 +65,7 @@ object ConfigPrinter : DocumentationPrinter<List<RuleSetPage>> {
if (configuration.isDefaultValueNonEmptyList()) {
list(configuration.name, configuration.getDefaultValueAsList())
} else {
keyValue { configuration.name to configuration.defaultValue }
keyValue { configuration.name to configuration.defaultValue.getQuotedIfNecessary() }
}
}

Expand Down
Expand Up @@ -68,8 +68,8 @@ object RuleSetPagePrinter : DocumentationPrinter<RuleSetPage> {
h4 { "Configuration options:" }
list {
rule.configuration.forEach {
val defaultValues = formatDefaultValues(it.defaultValue)
val defaultAndroidValues = it.defaultAndroidValue?.let(::formatDefaultValues)
val defaultValues = it.defaultValue.getQuotedIfNecessary()
val defaultAndroidValues = it.defaultAndroidValue?.getQuotedIfNecessary()
val defaultString = if (defaultAndroidValues != null) {
"(default: ${code { defaultValues }}) (android default: ${code { defaultAndroidValues }})"
} else {
Expand All @@ -94,10 +94,6 @@ object RuleSetPagePrinter : DocumentationPrinter<RuleSetPage> {
}
}

private fun formatDefaultValues(rawString: String) = rawString.lines().joinToString {
it.trim().removePrefix("- ")
}

private fun MarkdownContent.printRuleCodeExamples(rule: Rule) {
if (rule.nonCompliantCodeExample.isNotEmpty()) {
h4 { "Noncompliant Code:" }
Expand Down
@@ -1,5 +1,6 @@
package io.gitlab.arturbosch.detekt.generator.collection

import io.gitlab.arturbosch.detekt.generator.collection.DefaultValue.Companion.of
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatIllegalStateException
import org.spekframework.spek2.Spek
Expand All @@ -8,7 +9,7 @@ import org.spekframework.spek2.style.specification.describe
private val defaultConfiguration = Configuration(
name = "name",
description = "description",
defaultValue = "",
defaultValue = of(""),
defaultAndroidValue = null,
deprecated = null
)
Expand All @@ -17,7 +18,7 @@ object ConfigurationSpec : Spek({

describe("default value to list conversion") {
describe("empty default value") {
val subject by memoized { defaultConfiguration.copy(defaultValue = "") }
val subject by memoized { defaultConfiguration.copy(defaultValue = of("")) }

it("identifies default as not a list") {
assertThat(subject.isDefaultValueNonEmptyList()).isFalse()
Expand All @@ -28,7 +29,7 @@ object ConfigurationSpec : Spek({
}
}
describe("non list default value") {
val subject by memoized { defaultConfiguration.copy(defaultValue = "abc") }
val subject by memoized { defaultConfiguration.copy(defaultValue = of("abc")) }

it("identifies default as not a list") {
assertThat(subject.isDefaultValueNonEmptyList()).isFalse()
Expand All @@ -39,7 +40,7 @@ object ConfigurationSpec : Spek({
}
}
describe("empty list default value") {
val subject by memoized { defaultConfiguration.copy(defaultValue = "[ ]") }
val subject by memoized { defaultConfiguration.copy(defaultValue = of(emptyList())) }

it("identifies default as not a non empty list") {
assertThat(subject.isDefaultValueNonEmptyList()).isFalse()
Expand All @@ -50,7 +51,7 @@ object ConfigurationSpec : Spek({
}
}
describe("bracket list default value") {
val subject by memoized { defaultConfiguration.copy(defaultValue = "[ 'a', 'b' ]") }
val subject by memoized { defaultConfiguration.copy(defaultValue = of(listOf("a", "b"))) }

it("identifies default as a non empty list") {
assertThat(subject.isDefaultValueNonEmptyList()).isTrue()
Expand Down

0 comments on commit 8eabb07

Please sign in to comment.