Skip to content

Commit

Permalink
Validate yaml configurations by comparing their structure - detekt#516 (
Browse files Browse the repository at this point in the history
detekt#1998)

* Validate yaml configurations by comparing their structure - detekt#516

* Towards supporting composite config validation

* Generalize config validation interface

* Fix formatting

* Delete non existing rule configurations

* Support excluding config properties from validation

* Use settings#info to print invalid properties

* Exclude common rule properties by default from validation

* Regenerate default configuration

* Rename validation message functions
  • Loading branch information
arturbosch authored and Stanislav Myachenkov committed Dec 9, 2019
1 parent 5f49a72 commit 8dd4f33
Show file tree
Hide file tree
Showing 54 changed files with 686 additions and 38 deletions.
6 changes: 1 addition & 5 deletions config/detekt/detekt.yml
Expand Up @@ -53,8 +53,6 @@ formatting:
active: true
android: false
autoCorrect: true
NoItParamInMultilineLambda:
active: false
MaximumLineLength:
active: false

Expand Down Expand Up @@ -117,6 +115,7 @@ style:
ignoreHashCodeFunction: true
ignorePropertyDeclaration: true
ignoreAnnotation: true
ignoreEnums: true
ignoreNumbers: '-1,0,1,2,100,1000'
MayBeConst:
active: true
Expand All @@ -126,9 +125,6 @@ style:
active: true
SpacingBetweenPackageAndImports:
active: true
VariableNaming:
active: true
variablePattern: '(_)?[a-z][a-zA-Z0-9]*'
UnnecessaryAbstractClass:
active: true
UnnecessaryInheritance:
Expand Down
3 changes: 0 additions & 3 deletions config/detekt/format.yml
Expand Up @@ -2,8 +2,5 @@ formatting:
active: true
android: false
autoCorrect: true
# reports false positive for calls named 'it' in Spek
NoItParamInMultilineLambda:
active: false
MaximumLineLength:
active: false
@@ -1,9 +1,13 @@
package io.gitlab.arturbosch.detekt.api

import io.gitlab.arturbosch.detekt.api.internal.ValidatableConfiguration
import io.gitlab.arturbosch.detekt.api.internal.validateConfig

/**
* Wraps two different configuration which should be considered when retrieving properties.
*/
class CompositeConfig(private val lookFirst: Config, private val lookSecond: Config) : Config {
class CompositeConfig(private val lookFirst: Config, private val lookSecond: Config) :
Config, ValidatableConfiguration {

override fun subConfig(key: String): Config {
return CompositeConfig(lookFirst.subConfig(key), lookSecond.subConfig(key))
Expand All @@ -18,4 +22,10 @@ class CompositeConfig(private val lookFirst: Config, private val lookSecond: Con
}

override fun toString(): String = "CompositeConfig(lookFirst=$lookFirst, lookSecond=$lookSecond)"

/**
* Validates both sides of the composite config according to defined properties of the baseline config.
*/
override fun validate(baseline: Config, excludePatterns: Set<Regex>): List<Notification> =
validateConfig(lookFirst, baseline, excludePatterns) + validateConfig(lookSecond, baseline, excludePatterns)
}
Expand Up @@ -110,10 +110,10 @@ abstract class BaseConfig : HierarchicalConfig {
}
} catch (_: ClassCastException) {
error("Value \"$result\" set for config parameter \"${keySequence(key)}\" is not of" +
" required type ${default::class.simpleName}.")
" required type ${default::class.simpleName}.")
} catch (_: NumberFormatException) {
error("Value \"$result\" set for config parameter \"${keySequence(key)}\" is not of" +
" required type ${default::class.simpleName}.")
" required type ${default::class.simpleName}.")
}
}

Expand Down
Expand Up @@ -5,7 +5,7 @@ package io.gitlab.arturbosch.detekt.api
* Basic use cases are to specify different function or class names in the detekt
* yaml config and test for their appearance in specific rules.
*/
class SplitPattern(
open class SplitPattern(
text: String,
delimiters: String = ",",
removeTrailingAsterisks: Boolean = true
Expand Down
@@ -1,5 +1,7 @@
package io.gitlab.arturbosch.detekt.api

import io.gitlab.arturbosch.detekt.api.internal.ValidatableConfiguration
import io.gitlab.arturbosch.detekt.api.internal.validateConfig
import org.yaml.snakeyaml.Yaml
import java.io.BufferedReader
import java.net.URL
Expand All @@ -14,7 +16,7 @@ import java.nio.file.Path
class YamlConfig internal constructor(
val properties: Map<String, Any>,
override val parent: HierarchicalConfig.Parent?
) : BaseConfig() {
) : BaseConfig(), ValidatableConfiguration {

override fun subConfig(key: String): Config {
val subProperties = properties.getOrElse(key) { mapOf<String, Any>() }
Expand All @@ -34,6 +36,9 @@ class YamlConfig internal constructor(
return "YamlConfig(properties=$properties)"
}

override fun validate(baseline: Config, excludePatterns: Set<Regex>): List<Notification> =
validateConfig(this, baseline, excludePatterns)

companion object {

private const val YAML = ".yml"
Expand Down
@@ -0,0 +1,9 @@
package io.gitlab.arturbosch.detekt.api.internal

import io.gitlab.arturbosch.detekt.api.SplitPattern

class CommaSeparatedPattern(text: String, delimiters: String = ",") :
SplitPattern(text, delimiters, false) {

fun mapToRegex(): Set<Regex> = mapAll(::Regex).toSet()
}
@@ -1,10 +1,13 @@
package io.gitlab.arturbosch.detekt.cli
package io.gitlab.arturbosch.detekt.api.internal

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

@Suppress("UNCHECKED_CAST")
data class FailFastConfig(private val originalConfig: Config, private val defaultConfig: Config) : Config {
override fun subConfig(key: String) = FailFastConfig(originalConfig.subConfig(key), defaultConfig.subConfig(key))
data class FailFastConfig(private val originalConfig: Config, private val defaultConfig: Config) :
Config, ValidatableConfiguration {

override fun subConfig(key: String) =
FailFastConfig(originalConfig.subConfig(key), defaultConfig.subConfig(key))

override fun <T : Any> valueOrDefault(key: String, default: T): T {
return when (key) {
Expand All @@ -21,4 +24,7 @@ data class FailFastConfig(private val originalConfig: Config, private val defaul
else -> originalConfig.valueOrNull(key) ?: defaultConfig.valueOrNull(key)
}
}

override fun validate(baseline: Config, excludePatterns: Set<Regex>) =
validateConfig(originalConfig, baseline, excludePatterns)
}
Expand Up @@ -2,4 +2,7 @@ package io.gitlab.arturbosch.detekt.api.internal

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

data class SimpleNotification(override val message: String) : Notification
data class SimpleNotification(override val message: String) : Notification {

override fun toString(): String = message
}
@@ -0,0 +1,76 @@
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

interface ValidatableConfiguration {

fun validate(baseline: Config, excludePatterns: Set<Regex>): List<Notification>
}

/**
* Known existing properties on rule's which my be absent in the default-detekt-config.yml.
*
* We need to predefine them as the user may not have already declared an 'config'-block
* in the configuration and we want to validate the config by default.
*/
const val DEFAULT_PROPERTY_EXCLUDES = ".*>.*>excludes,.*>.*>includes,.*>.*>active,.*>.*>autoCorrect"

@Suppress("UNCHECKED_CAST", "ComplexMethod")
fun validateConfig(
config: Config,
baseline: Config,
excludePatterns: Set<Regex> = CommaSeparatedPattern(DEFAULT_PROPERTY_EXCLUDES).mapToRegex()
): List<Notification> {
require(baseline != Config.empty) { "Cannot validate configuration based on an empty baseline config." }
require(baseline is YamlConfig) { "Only supported baseline config is the YamlConfig." }

if (config == Config.empty) {
return emptyList()
}

val notifications = mutableListOf<Notification>()

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 (excludePatterns.any { it.matches(propertyPath) }) {
continue
}

if (!base.contains(prop)) {
notifications.add(propertyDoesNotExists(propertyPath))
}

val next = current[prop] as? Map<String, Any>
val nextBase = base[prop] as? Map<String, Any>

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

when (config) {
is YamlConfig -> testKeys(config.properties, baseline.properties, null)
is ValidatableConfiguration -> notifications.addAll(config.validate(baseline, excludePatterns))
else -> error("Unsupported config type for validation: '${config::class}'.")
}

return notifications
}

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

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

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

import io.gitlab.arturbosch.detekt.api.CompositeConfig
import io.gitlab.arturbosch.detekt.api.Config
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") {
val result = validateConfig(baseline, baseline)
assertThat(result).isEmpty()
}

it("passes for properties which may appear on rules but may be not present in default config") {
val result = validateConfig(
yamlConfig("config_validation/default-excluded-properties.yml"),
baseline
)
assertThat(result).isEmpty()
}

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

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

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

it("reports unexpected nested configs") {
// note that the baseline config is now the first argument
val result = validateConfig(baseline, yamlConfig("config_validation/no-value.yml"))
assertThat(result).contains(
unexpectedNestedConfiguration("style"),
unexpectedNestedConfiguration("comments")
)
}

describe("validate composite configurations") {

it("passes for same left, right and baseline config") {
val result = validateConfig(CompositeConfig(baseline, baseline), baseline)
assertThat(result).isEmpty()
}

it("passes for empty configs") {
val result = validateConfig(CompositeConfig(Config.empty, Config.empty), baseline)
assertThat(result).isEmpty()
}

it("finds accumulated errors") {
val result = validateConfig(
CompositeConfig(
yamlConfig("config_validation/other-nested-property-names.yml"),
yamlConfig("config_validation/no-nested-config.yml")
),
baseline
)

assertThat(result).contains(
nestedConfigurationExpected("complexity"),
nestedConfigurationExpected("style>WildcardImport"),
propertyDoesNotExists("complexity>LongLongMethod"),
propertyDoesNotExists("complexity>LongParameterList>enabled"),
propertyDoesNotExists("complexity>LargeClass>howMany"),
propertyDoesNotExists("complexity>InnerMap>InnerKey"),
propertyDoesNotExists("complexity>InnerMap>Inner2>nestedActive")
)
}
}

describe("configure additional exclude paths") {

fun patterns(str: String) = CommaSeparatedPattern(str).mapToRegex()

it("does not report any complexity properties") {
val result = validateConfig(
yamlConfig("config_validation/other-nested-property-names.yml"),
baseline,
patterns("complexity")
)
assertThat(result).isEmpty()
}

it("does not report 'complexity>LargeClass>howMany'") {
val result = validateConfig(
yamlConfig("config_validation/other-nested-property-names.yml"),
baseline,
patterns(".*>.*>howMany")
)

assertThat(result).contains(
propertyDoesNotExists("complexity>LongLongMethod"),
propertyDoesNotExists("complexity>LongParameterList>enabled"),
propertyDoesNotExists("complexity>InnerMap>InnerKey"),
propertyDoesNotExists("complexity>InnerMap>Inner2>nestedActive")
)

assertThat(result).doesNotContain(
propertyDoesNotExists("complexity>LargeClass>howMany")
)
}

it("does not report '.*>InnerMap'") {
val result = validateConfig(
yamlConfig("config_validation/other-nested-property-names.yml"),
baseline,
patterns(".*>InnerMap")
)

assertThat(result).contains(
propertyDoesNotExists("complexity>LargeClass>howMany"),
propertyDoesNotExists("complexity>LongLongMethod"),
propertyDoesNotExists("complexity>LongParameterList>enabled")
)

assertThat(result).doesNotContain(
propertyDoesNotExists("complexity>InnerMap>InnerKey"),
propertyDoesNotExists("complexity>InnerMap>Inner2>nestedActive")
)
}
}
}
})

0 comments on commit 8dd4f33

Please sign in to comment.