Skip to content

Commit

Permalink
Validate yaml configurations by comparing their structure - #516
Browse files Browse the repository at this point in the history
  • Loading branch information
arturbosch committed Oct 8, 2019
1 parent 8daf49e commit 05e6df8
Show file tree
Hide file tree
Showing 8 changed files with 191 additions and 0 deletions.
@@ -1,5 +1,9 @@
package io.gitlab.arturbosch.detekt.api.internal

import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.Notification
import io.gitlab.arturbosch.detekt.api.YamlConfig

private val identifierRegex = Regex("[aA-zZ]+([-][aA-zZ]+)*")

/**
Expand All @@ -8,3 +12,47 @@ private val identifierRegex = Regex("[aA-zZ]+([-][aA-zZ]+)*")
internal fun validateIdentifier(id: String) {
require(id.matches(identifierRegex)) { "id must match [aA-zZ]+([-][aA-zZ]+)*" }
}

fun verifyConfig(config: Config, baseline: Config): List<Notification> {
if (config == Config.empty && baseline == Config.empty) {
return emptyList()
}

val notifications = mutableListOf<Notification>()

@Suppress("UNCHECKED_CAST")
fun testKeys(current: Map<String, Any>, base: Map<String, Any>, parentPath: String?) {
for (prop in current.keys) {

val propertyPath = "${if (parentPath == null) "" else "$parentPath>"}$prop"
if (!base.contains(prop)) {
notifications.add(doesNotExistsMessage(propertyPath))
}

val next = current[prop]?.let { it as? Map<String, Any> }
val nextBase = base[prop]?.let { it as? Map<String, Any> }

when {
next == null && nextBase != null -> notifications.add(nestedConfigExpectedMessage(propertyPath))
base.contains(prop) && next != null && nextBase == null ->
notifications.add(unexpectedNestedConfigMessage(propertyPath))
next != null && nextBase != null -> testKeys(next, nextBase, propertyPath)
}
}
}

if (config is YamlConfig && baseline is YamlConfig) {
testKeys(config.properties, baseline.properties, null)
}

return notifications
}

internal fun doesNotExistsMessage(prop: String): Notification =
SimpleNotification("Property '$prop' is misspelled or does not exist.")

internal fun nestedConfigExpectedMessage(prop: String): Notification =
SimpleNotification("Nested config expected for '$prop'.")

internal fun unexpectedNestedConfigMessage(prop: String): Notification =
SimpleNotification("Unexpected nested config for '$prop'.")
@@ -0,0 +1,60 @@
package io.gitlab.arturbosch.detekt.api.internal

import io.gitlab.arturbosch.detekt.test.yamlConfig
import org.assertj.core.api.Assertions.assertThat
import org.spekframework.spek2.Spek
import org.spekframework.spek2.style.specification.describe

internal class ConfigValidationSpec : Spek({

describe("validate configuration file") {

val baseline = yamlConfig("config_validation/baseline.yml")

it("passes for same config test") {
verifyConfig(baseline, baseline)
}

it("reports different rule set name") {
val result = verifyConfig(
yamlConfig("config_validation/other-ruleset-name.yml"),
baseline
)
assertThat(result).contains(doesNotExistsMessage("code-smell"))
}

it("reports different nested property names") {
val result = verifyConfig(
yamlConfig("config_validation/other-nested-property-names.yml"),
baseline
)
assertThat(result).contains(
doesNotExistsMessage("complexity>LongLongMethod"),
doesNotExistsMessage("complexity>LongParameterList>enabled"),
doesNotExistsMessage("complexity>LargeClass>howMany"),
doesNotExistsMessage("complexity>InnerMap>InnerKey"),
doesNotExistsMessage("complexity>InnerMap>Inner2>nestedActive")
)
}

it("reports different rule set name") {
val result = verifyConfig(
yamlConfig("config_validation/no-nested-config.yml"),
baseline
)
assertThat(result).contains(
nestedConfigExpectedMessage("complexity"),
nestedConfigExpectedMessage("style>WildcardImport")
)
}

it("reports unexpected nested configs") {
// note that the baseline config is now the first argument
val result = verifyConfig(baseline, yamlConfig("config_validation/no-value.yml"))
assertThat(result).contains(
unexpectedNestedConfigMessage("style"),
unexpectedNestedConfigMessage("comments")
)
}
}
})
27 changes: 27 additions & 0 deletions detekt-api/src/test/resources/config_validation/baseline.yml
@@ -0,0 +1,27 @@
complexity:
LongMethod:
active: true
threshold: 20
LongParameterList:
active: false
threshold: 5
LargeClass:
active: false
threshold: 70
InnerMap:
Inner1:
active: true
Inner2:
active: true

style:
WildcardImport:
active: true
NoElseInWhenExpression:
active: true
MagicNumber:
active: true
ignoreNumbers: '-1,0,1,2'

comments:
active: false
@@ -0,0 +1,4 @@
complexity: false

style:
WildcardImport: false
3 changes: 3 additions & 0 deletions detekt-api/src/test/resources/config_validation/no-value.yml
@@ -0,0 +1,3 @@
complexity:
style:
comments:
@@ -0,0 +1,15 @@
complexity:
LongLongMethod:
active: true
threshold: 20
LongParameterList:
enabled: false
threshold: 5
LargeClass:
active: false
howMany: 70
InnerMap:
InnerKey:
active: true
Inner2:
nestedActive: true
@@ -0,0 +1,15 @@
code-smell:
LongMethod:
active: true
threshold: 20
LongParameterList:
active: false
threshold: 5
LargeClass:
active: false
threshold: 70
InnerMap:
Inner1:
active: true
Inner2:
active: true
@@ -0,0 +1,19 @@
package io.gitlab.arturbosch.detekt.cli

import io.gitlab.arturbosch.detekt.api.internal.verifyConfig
import io.gitlab.arturbosch.detekt.test.yamlConfig
import org.assertj.core.api.Assertions.assertThat
import org.spekframework.spek2.Spek
import org.spekframework.spek2.style.specification.describe

class DefaultConfigValidationSpec : Spek({

describe("default configuration is valid") {

it("is valid comparing itself") {
val baseline = yamlConfig("default-detekt-config.yml")
val result = verifyConfig(baseline, baseline)
assertThat(result).isEmpty()
}
}
})

0 comments on commit 05e6df8

Please sign in to comment.