/
ValidateConfig.kt
153 lines (129 loc) · 5.35 KB
/
ValidateConfig.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
package io.gitlab.arturbosch.detekt.core.config
import io.github.detekt.utils.openSafeStream
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.Notification
import io.gitlab.arturbosch.detekt.api.internal.CommaSeparatedPattern
import io.gitlab.arturbosch.detekt.api.internal.SimpleNotification
import java.util.Properties
/**
* 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.
*/
val DEFAULT_PROPERTY_EXCLUDES = setOf(
".*>excludes",
".*>includes",
".*>active",
".*>.*>excludes",
".*>.*>includes",
".*>.*>active",
".*>.*>autoCorrect",
".*>severity",
".*>.*>severity",
"build>weights.*",
".*>.*>ignoreAnnotated",
".*>.*>ignoreFunction",
).joinToString(",")
fun validateConfig(
config: Config,
baseline: Config,
excludePatterns: Set<Regex> = CommaSeparatedPattern(DEFAULT_PROPERTY_EXCLUDES).mapToRegex()
): List<Notification> = validateConfig(
config,
baseline,
ValidationSettings(
config.subConfig("config").valueOrDefault("warningsAsErrors", false),
excludePatterns,
)
)
internal data class ValidationSettings(
val warningsAsErrors: Boolean,
val excludePatterns: Set<Regex>,
)
@Suppress("UNCHECKED_CAST", "ComplexMethod")
internal fun validateConfig(
config: Config,
baseline: Config,
settings: ValidationSettings,
): 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()
}
val (warningsAsErrors, excludePatterns) = settings
val notifications = mutableListOf<Notification>()
fun getDeprecatedProperties(): List<Pair<Regex, String>> {
return settings.javaClass.classLoader
.getResource("deprecation.properties")!!
.openSafeStream()
.use { inputStream ->
val prop = Properties().apply { load(inputStream) }
prop.entries.map { entry ->
(entry.key as String).toRegex() to (entry.value as String)
}
}
}
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"
val deprecationWarning = getDeprecatedProperties()
.find { (regex, _) -> regex.matches(propertyPath) }
?.second
val isExcluded = excludePatterns.any { it.matches(propertyPath) }
if (deprecationWarning != null) {
notifications.add(propertyIsDeprecated(propertyPath, deprecationWarning, warningsAsErrors))
}
if (deprecationWarning != null || isExcluded) {
continue
}
if (!base.contains(prop)) {
notifications.add(propertyDoesNotExists(propertyPath))
} else if (current[prop] is String && base[prop] is List<*>) {
notifications.add(propertyShouldBeAnArray(propertyPath, warningsAsErrors))
}
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'.")
internal fun propertyIsDeprecated(
prop: String,
deprecationDescription: String,
reportAsError: Boolean,
): Notification =
SimpleNotification(
"Property '$prop' is deprecated. $deprecationDescription.",
if (reportAsError) Notification.Level.Error else Notification.Level.Warning,
)
internal fun propertyShouldBeAnArray(
prop: String,
reportAsError: Boolean,
): Notification =
SimpleNotification(
"Property '$prop' should be an array instead of a String.",
if (reportAsError) Notification.Level.Error else Notification.Level.Warning,
)