Skip to content

Commit

Permalink
Add exhaustiveness check for config validation (#5089)
Browse files Browse the repository at this point in the history
* Add exhaustiveness check for config validation

* fix ArgumentListWrapping

* inject deprecated properties to validators

* move converting warning level up the chain

* remove exclusion pattern from abstract validator

* fix EmptyDefaultConstructor

Co-authored-by: Markus Schwarz <post@markus-schwarz.net>
  • Loading branch information
marschwar and Markus Schwarz committed Aug 21, 2022
1 parent b8fbc8e commit 2645549
Show file tree
Hide file tree
Showing 33 changed files with 746 additions and 343 deletions.
@@ -1,6 +1,8 @@
package io.gitlab.arturbosch.detekt.core.config

import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.core.config.validation.ValidatableConfiguration
import io.gitlab.arturbosch.detekt.core.config.validation.validateConfig

@Suppress("UNCHECKED_CAST")
internal data class AllRulesConfig(
Expand Down
Expand Up @@ -2,6 +2,8 @@ package io.gitlab.arturbosch.detekt.core.config

import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.Notification
import io.gitlab.arturbosch.detekt.core.config.validation.ValidatableConfiguration
import io.gitlab.arturbosch.detekt.core.config.validation.validateConfig

/**
* Wraps two different configuration which should be considered when retrieving properties.
Expand Down

This file was deleted.

Expand Up @@ -2,6 +2,8 @@ package io.gitlab.arturbosch.detekt.core.config

import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.Notification
import io.gitlab.arturbosch.detekt.core.config.validation.ValidatableConfiguration
import io.gitlab.arturbosch.detekt.core.config.validation.validateConfig

@Suppress("UNCHECKED_CAST")
class DisabledAutoCorrectConfig(private val wrapped: Config) : Config, ValidatableConfiguration {
Expand Down

This file was deleted.

Expand Up @@ -5,6 +5,8 @@ package io.gitlab.arturbosch.detekt.core.config
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.Config.Companion.CONFIG_SEPARATOR
import io.gitlab.arturbosch.detekt.api.Notification
import io.gitlab.arturbosch.detekt.core.config.validation.ValidatableConfiguration
import io.gitlab.arturbosch.detekt.core.config.validation.validateConfig
import org.yaml.snakeyaml.Yaml
import java.io.Reader
import java.nio.file.Path
Expand Down
@@ -0,0 +1,28 @@
package io.gitlab.arturbosch.detekt.core.config.validation

import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.ConfigValidator
import io.gitlab.arturbosch.detekt.api.Notification
import io.gitlab.arturbosch.detekt.core.config.YamlConfig

internal abstract class AbstractYamlConfigValidator : ConfigValidator {

override fun validate(config: Config): Collection<Notification> {
require(config is YamlConfig) {
val yamlConfigClass = YamlConfig::class.simpleName
val actualClass = config.javaClass.simpleName

"Only supported config is the $yamlConfigClass. Actual type is $actualClass"
}
val settings = ValidationSettings(
config.subConfig("config").valueOrDefault("checkExhaustiveness", false),
)

return validate(config, settings)
}

abstract fun validate(
configToValidate: YamlConfig,
settings: ValidationSettings
): Collection<Notification>
}
@@ -0,0 +1,118 @@
package io.gitlab.arturbosch.detekt.core.config.validation

import io.github.detekt.tooling.api.InvalidConfig
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.ConfigValidator
import io.gitlab.arturbosch.detekt.api.Notification
import io.gitlab.arturbosch.detekt.api.Notification.Level
import io.gitlab.arturbosch.detekt.api.internal.SimpleNotification
import io.gitlab.arturbosch.detekt.core.NL
import io.gitlab.arturbosch.detekt.core.ProcessingSettings
import io.gitlab.arturbosch.detekt.core.config.YamlConfig
import io.gitlab.arturbosch.detekt.core.extensions.loadExtensions
import io.gitlab.arturbosch.detekt.core.reporting.red
import io.gitlab.arturbosch.detekt.core.reporting.yellow

/**
* 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.
*/
internal val DEFAULT_PROPERTY_EXCLUDES = setOf(
".*>excludes",
".*>includes",
".*>active",
".*>.*>excludes",
".*>.*>includes",
".*>.*>active",
".*>.*>autoCorrect",
".*>severity",
".*>.*>severity",
"build>weights.*",
".*>.*>ignoreAnnotated",
".*>.*>ignoreFunction",
).joinToString(",")

internal fun checkConfiguration(settings: ProcessingSettings, baseline: Config) {
var shouldValidate = settings.spec.configSpec.shouldValidateBeforeAnalysis
if (shouldValidate == null) {
val props = settings.config.subConfig("config")
shouldValidate = props.valueOrDefault("validation", true)
}
if (shouldValidate) {
val validators =
loadExtensions<ConfigValidator>(settings) + DefaultPropertiesConfigValidator(settings, baseline)
val notifications = validators.flatMap { it.validate(settings.config) }
notifications.map(Notification::message).forEach(settings::info)
val errors = notifications.filter(Notification::isError)
if (errors.isNotEmpty()) {
val problems = notifications.joinToString(NL) { "\t- ${it.renderMessage()}" }
val propsString = if (errors.size == 1) "property" else "properties"
val title = "Run failed with ${errors.size} invalid config $propsString.".red()
throw InvalidConfig("$title$NL$problems")
}
}
}

internal fun validateConfig(
config: Config,
baseline: Config,
excludePatterns: Set<Regex>
): List<Notification> {
require(baseline != Config.empty) { "Cannot validate configuration based on an empty baseline config." }
require(baseline is YamlConfig) {
val yamlConfigClass = YamlConfig::class.simpleName
val actualClass = baseline.javaClass.simpleName

"Only supported baseline config is the $yamlConfigClass. Actual type is $actualClass"
}

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

return when (config) {
is YamlConfig -> validateYamlConfig(config, baseline, excludePatterns)
is ValidatableConfiguration -> config.validate(baseline, excludePatterns)
else -> error("Unsupported config type for validation: '${config::class}'.")
}
}

private fun validateYamlConfig(
configToValidate: YamlConfig,
baseline: YamlConfig,
excludePatterns: Set<Regex>
): List<Notification> {
val deprecatedProperties = loadDeprecations()
val warningsAsErrors = configToValidate
.subConfig("config")
.valueOrDefault("warningsAsErrors", false)

val validators: List<ConfigValidator> = listOf(
InvalidPropertiesConfigValidator(baseline, deprecatedProperties.keys, excludePatterns),
DeprecatedPropertiesConfigValidator(deprecatedProperties),
MissingRulesConfigValidator(baseline, excludePatterns)
)

return validators
.flatMap { it.validate(configToValidate) }
.map { notification ->
notification.transformIf(warningsAsErrors && notification.level == Level.Warning) {
SimpleNotification(
message = notification.message,
level = Level.Error
)
}
}
}

private fun <T> T.transformIf(condition: Boolean, transform: () -> T): T =
if (condition) transform() else this

internal fun Notification.renderMessage(): String =
when (level) {
Level.Error -> message.red()
Level.Warning -> message.yellow()
Level.Info -> message
}
@@ -1,4 +1,4 @@
package io.gitlab.arturbosch.detekt.core.config
package io.gitlab.arturbosch.detekt.core.config.validation

import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.ConfigValidator
Expand All @@ -8,7 +8,7 @@ import io.gitlab.arturbosch.detekt.api.internal.DefaultRuleSetProvider
import io.gitlab.arturbosch.detekt.core.ProcessingSettings
import io.gitlab.arturbosch.detekt.core.rules.RuleSetLocator

class DefaultPropertiesConfigValidator(
internal class DefaultPropertiesConfigValidator(
private val settings: ProcessingSettings,
private val baseline: Config,
) : ConfigValidator {
Expand Down

0 comments on commit 2645549

Please sign in to comment.