-
-
Notifications
You must be signed in to change notification settings - Fork 758
/
ConfigurationCollector.kt
266 lines (224 loc) · 12.5 KB
/
ConfigurationCollector.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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
package io.gitlab.arturbosch.detekt.generator.collection
import io.gitlab.arturbosch.detekt.api.ValueWithReason
import io.gitlab.arturbosch.detekt.api.valuesWithReason
import io.gitlab.arturbosch.detekt.generator.collection.ConfigurationCollector.ConfigWithAndroidVariantsSupport.ANDROID_VARIANTS_DELEGATE_NAME
import io.gitlab.arturbosch.detekt.generator.collection.ConfigurationCollector.ConfigWithAndroidVariantsSupport.DEFAULT_ANDROID_VALUE_ARGUMENT_NAME
import io.gitlab.arturbosch.detekt.generator.collection.ConfigurationCollector.ConfigWithAndroidVariantsSupport.isAndroidVariantConfigDelegate
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.ConfigurationCollector.StringListSupport.getListDefaultOrNull
import io.gitlab.arturbosch.detekt.generator.collection.ConfigurationCollector.StringListSupport.hasListDeclaration
import io.gitlab.arturbosch.detekt.generator.collection.ConfigurationCollector.ValuesWithReasonSupport.getValuesWithReasonDefaultOrNull
import io.gitlab.arturbosch.detekt.generator.collection.ConfigurationCollector.ValuesWithReasonSupport.hasValuesWithReasonDeclaration
import io.gitlab.arturbosch.detekt.generator.collection.exception.InvalidDocumentationException
import org.jetbrains.kotlin.psi.KtBinaryExpression
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtCallableReferenceExpression
import org.jetbrains.kotlin.psi.KtConstantExpression
import org.jetbrains.kotlin.psi.KtElement
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtObjectDeclaration
import org.jetbrains.kotlin.psi.KtProperty
import org.jetbrains.kotlin.psi.KtStringTemplateExpression
import org.jetbrains.kotlin.psi.KtValueArgument
import org.jetbrains.kotlin.psi.psiUtil.anyDescendantOfType
import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType
import org.jetbrains.kotlin.psi.psiUtil.findDescendantOfType
import org.jetbrains.kotlin.psi.psiUtil.referenceExpression
import io.gitlab.arturbosch.detekt.api.internal.Configuration as ConfigAnnotation
class ConfigurationCollector {
private val constantsByName = mutableMapOf<String, DefaultValue>()
private val properties = mutableListOf<KtProperty>()
fun getConfiguration(): List<Configuration> {
return properties.mapNotNull { it.parseConfigurationAnnotation() }
}
fun addProperty(prop: KtProperty) {
properties.add(prop)
}
fun addCompanion(aRuleCompanion: KtObjectDeclaration) {
constantsByName.putAll(
aRuleCompanion
.collectDescendantsOfType<KtProperty>()
.mapNotNull(::resolveConstantOrNull)
)
}
private fun resolveConstantOrNull(prop: KtProperty): Pair<String, DefaultValue>? {
if (prop.isVar) return null
val propertyName = checkNotNull(prop.name)
val constantOrNull = prop.getConstantValue()
return constantOrNull?.let { propertyName to it }
}
private fun KtProperty.getConstantValue(): DefaultValue? {
if (hasValuesWithReasonDeclaration()) {
return getValuesWithReasonDefaultOrNull()
?: invalidDocumentation { "Invalid declaration of values with reasons default for property '$text'" }
}
if (hasListDeclaration()) {
return getListDefaultOrNull(emptyMap())
?: invalidDocumentation { "Invalid declaration of string list default for property '$text'" }
}
return findDescendantOfType<KtConstantExpression>()?.toDefaultValueIfLiteral()
?: findDescendantOfType<KtStringTemplateExpression>()?.toDefaultValueIfLiteral()
}
private fun KtProperty.parseConfigurationAnnotation(): Configuration? = when {
isAnnotatedWith(ConfigAnnotation::class) -> toConfiguration()
isInitializedWithConfigDelegate() -> invalidDocumentation {
"'$name' is using the config delegate but is not annotated with @Configuration"
}
else -> null
}
private fun KtProperty.toConfiguration(): Configuration {
if (!isInitializedWithConfigDelegate()) {
invalidDocumentation { "'$name' is not using one of the config property delegates ($DELEGATE_NAMES)" }
}
if (isFallbackConfigDelegate()) {
checkUsingInvalidFallbackReference(properties)
}
val propertyName: String = checkNotNull(name)
val deprecationMessage = firstAnnotationParameterOrNull(Deprecated::class)
val description: String = firstAnnotationParameter(ConfigAnnotation::class)
val defaultValue = getDefaultValue(constantsByName)
val defaultAndroidValue = getAndroidDefaultValue(constantsByName)
return Configuration(
name = propertyName,
description = description,
defaultValue = defaultValue,
defaultAndroidValue = defaultAndroidValue,
deprecated = deprecationMessage,
)
}
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 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)
}
fun KtExpression.toDefaultValue(constantsByName: Map<String, DefaultValue>): DefaultValue {
return getValuesWithReasonDefaultOrNull()
?: getListDefaultOrNull(constantsByName)
?: toDefaultValueIfLiteral()
?: constantsByName[text.withoutQuotes()]
?: error("$text is neither a literal nor a constant")
}
fun KtExpression.toDefaultValueIfLiteral(): DefaultValue? = createDefaultValueIfLiteral(text)
}
private object ConfigWithFallbackSupport {
const val FALLBACK_DELEGATE_NAME = "configWithFallback"
private const val FALLBACK_ARGUMENT_NAME = "fallbackProperty"
fun KtProperty.isFallbackConfigDelegate(): Boolean =
delegate?.expression?.referenceExpression()?.text == FALLBACK_DELEGATE_NAME
fun KtProperty.checkUsingInvalidFallbackReference(properties: List<KtProperty>) {
val fallbackPropertyReference = getValueArgument(
name = FALLBACK_ARGUMENT_NAME,
actionForPositionalMatch = { it.first() }
)?.getReferenceIdentifierOrNull()
val fallbackProperty = properties.find { it.name == fallbackPropertyReference }
if (fallbackProperty == null || !fallbackProperty.isInitializedWithConfigDelegate()) {
invalidDocumentation {
"The fallback property '$fallbackPropertyReference' of property '$name' " +
"must also be defined using a config property delegate "
}
}
}
private fun KtValueArgument.getReferenceIdentifierOrNull(): String? =
(getArgumentExpression() as? KtCallableReferenceExpression)
?.callableReference
?.getIdentifier()
?.text
}
private object ConfigWithAndroidVariantsSupport {
const val ANDROID_VARIANTS_DELEGATE_NAME = "configWithAndroidVariants"
const val DEFAULT_ANDROID_VALUE_ARGUMENT_NAME = "defaultAndroidValue"
fun KtProperty.isAndroidVariantConfigDelegate(): Boolean =
delegate?.expression?.referenceExpression()?.text == ANDROID_VARIANTS_DELEGATE_NAME
}
private object ValuesWithReasonSupport {
private const val VALUES_WITH_REASON_FACTORY_METHOD = "valuesWithReason"
fun KtElement.getValuesWithReasonDefaultOrNull(): DefaultValue? {
return getValuesWithReasonDeclarationOrNull()
?.valueArguments
?.map(::toValueWithReason)
?.let { DefaultValue.of(valuesWithReason(it)) }
}
fun KtElement.getValuesWithReasonDeclarationOrNull(): KtCallExpression? =
findDescendantOfType { it.isValuesWithReasonDeclaration() }
fun KtCallExpression.isValuesWithReasonDeclaration(): Boolean {
return referenceExpression()?.text == VALUES_WITH_REASON_FACTORY_METHOD
}
fun KtProperty.hasValuesWithReasonDeclaration(): Boolean =
anyDescendantOfType<KtCallExpression> { it.isValuesWithReasonDeclaration() }
private fun toValueWithReason(arg: KtValueArgument): ValueWithReason {
val keyToValue = arg.children.first() as? KtBinaryExpression
return keyToValue?.let {
ValueWithReason(
value = it.left?.text?.withoutQuotes()
?: error("left side of value with reason argument is null"),
reason = it.right?.text?.withoutQuotes()
?: error("right side of value with reason argument is null")
)
} ?: error("invalid value argument '${arg.text}'")
}
}
private object StringListSupport {
private const val LIST_OF = "listOf"
private const val EMPTY_LIST = "emptyList"
private val LIST_CREATORS = setOf(LIST_OF, EMPTY_LIST)
fun KtElement.getListDefaultOrNull(constantsByName: Map<String, DefaultValue>): DefaultValue? {
return getListDeclarationOrNull()?.valueArguments?.map {
(constantsByName[it.text]?.getPlainValue() ?: it.text.withoutQuotes())
}?.let { DefaultValue.of(it) }
}
fun KtElement.getListDeclarationOrNull(): KtCallExpression? =
findDescendantOfType { it.isListDeclaration() }
fun KtProperty.hasListDeclaration(): Boolean =
anyDescendantOfType<KtCallExpression> { it.isListDeclaration() }
fun KtCallExpression.isListDeclaration() =
referenceExpression()?.text in LIST_CREATORS
}
companion object {
private const val SIMPLE_DELEGATE_NAME = "config"
private val DELEGATE_NAMES = listOf(
SIMPLE_DELEGATE_NAME,
FALLBACK_DELEGATE_NAME,
ANDROID_VARIANTS_DELEGATE_NAME
)
private const val DEFAULT_VALUE_ARGUMENT_NAME = "defaultValue"
private fun KtProperty.isInitializedWithConfigDelegate(): Boolean =
delegate?.expression?.referenceExpression()?.text in DELEGATE_NAMES
private fun KtElement.invalidDocumentation(message: () -> String): Nothing {
throw InvalidDocumentationException("[${containingFile.name}] ${message.invoke()}")
}
private fun KtProperty.getValueArgument(
name: String,
actionForPositionalMatch: (List<KtValueArgument>) -> KtValueArgument?
): KtValueArgument? {
val callExpression = delegate?.expression as? KtCallExpression ?: return null
val arguments = callExpression.valueArguments
return arguments.find { it.getArgumentName()?.text == name } ?: actionForPositionalMatch(arguments)
}
}
}