Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce DefaultValue type #3928

Merged
merged 7 commits into from Jan 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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