Skip to content

Commit

Permalink
Introduce DefaultValue type (#3928)
Browse files Browse the repository at this point in the history
* Introduce DefaultValue type

* reuse createDefaultValueIfLiteral in RuleSetProviderCollector.kt

* use DefaultValue.of() explicitly

* default getAsPlainString to toString

* rename ListDefault to StringListDefault

Co-authored-by: Markus Schwarz <post@markus-schwarz.net>
  • Loading branch information
marschwar and Markus Schwarz committed Jan 23, 2022
1 parent 31520da commit 8a69676
Show file tree
Hide file tree
Showing 14 changed files with 210 additions and 140 deletions.
4 changes: 2 additions & 2 deletions config/detekt/detekt.yml
Expand Up @@ -104,12 +104,12 @@ formatting:

naming:
ClassNaming:
excludes: [ '**/*Spec.kt' ]
excludes: ['**/*Spec.kt']
TopLevelPropertyNaming:
constantPattern: '[a-z][_A-Za-z0-9]*|[A-Z][_A-Z0-9]*'
InvalidPackageDeclaration:
active: true
excludes: [ '**/build-logic/**/*.kt', '**/*.kts' ]
excludes: ['**/build-logic/**/*.kt', '**/*.kts']
NoNameShadowing:
active: true
NonBooleanPropertyPrefixedWithIs:
Expand Down
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,9 @@ 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.exception.InvalidDocumentationException
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtCallableReferenceExpression
Expand All @@ -24,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 @@ -43,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 DefaultValue.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 @@ -84,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 DefaultValue.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 @@ -186,14 +188,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 = toString()
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 = StringListDefault(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 StringListDefault(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 @@ -87,6 +87,8 @@ class RuleSetProviderVisitor : DetektVisitor() {

override fun visitProperty(property: KtProperty) {
super.visitProperty(property)
if (!containsRuleSetProvider) return

if (property.isOverride() && property.name != null && property.name == PROPERTY_RULE_SET_ID) {
name = (property.initializer as? KtStringTemplateExpression)?.entries?.get(0)?.text
?: throw InvalidDocumentationException(
Expand All @@ -95,7 +97,8 @@ class RuleSetProviderVisitor : DetektVisitor() {
)
}
if (property.isAnnotatedWith(ConfigAnnotation::class)) {
val defaultValue = formatDefaultValue(
val defaultValue = toDefaultValue(
name,
checkNotNull(property.delegate?.expression as? KtCallExpression)
.valueArguments
.first()
Expand Down Expand Up @@ -133,14 +136,11 @@ class RuleSetProviderVisitor : DetektVisitor() {
}

companion object {

private const val DOUBLE_QUOTE = '"'

private fun formatDefaultValue(defaultValueText: String): String =
if (defaultValueText.startsWith(DOUBLE_QUOTE) && defaultValueText.endsWith(DOUBLE_QUOTE)) {
"'${defaultValueText.removeSurrounding("$DOUBLE_QUOTE")}'"
} else {
defaultValueText
}
private fun toDefaultValue(providerName: String, defaultValueText: String): DefaultValue =
createDefaultValueIfLiteral(defaultValueText)
?: throw InvalidDocumentationException(
"Unsupported default value format '$defaultValueText' " +
"in $providerName. Please use a Boolean, Int or String literal instead."
)
}
}
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(RuleSetPagePrinter::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
Expand Up @@ -66,7 +66,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

0 comments on commit 8a69676

Please sign in to comment.