Skip to content

Commit 54710a8

Browse files
Add "DataClassTypedIDs" rule
1 parent 6096461 commit 54710a8

File tree

4 files changed

+119
-0
lines changed

4 files changed

+119
-0
lines changed

src/main/kotlin/com/github/ivy/explicit/IvyExplicitRuleSetProvider.kt

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.github.ivy.explicit
22

33
import com.github.ivy.explicit.rule.DataClassDefaultValuesRule
44
import com.github.ivy.explicit.rule.DataClassFunctionsRule
5+
import com.github.ivy.explicit.rule.DataClassTypedIDsRule
56
import io.gitlab.arturbosch.detekt.api.Config
67
import io.gitlab.arturbosch.detekt.api.RuleSet
78
import io.gitlab.arturbosch.detekt.api.RuleSetProvider
@@ -15,6 +16,7 @@ class IvyExplicitRuleSetProvider : RuleSetProvider {
1516
listOf(
1617
DataClassFunctionsRule(config),
1718
DataClassDefaultValuesRule(config),
19+
DataClassTypedIDsRule(config)
1820
),
1921
)
2022
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package com.github.ivy.explicit.rule
2+
3+
import io.gitlab.arturbosch.detekt.api.*
4+
import io.gitlab.arturbosch.detekt.rules.isOverride
5+
import org.jetbrains.kotlin.psi.KtClass
6+
import org.jetbrains.kotlin.psi.KtParameter
7+
8+
class DataClassTypedIDsRule(config: Config) : Rule(config) {
9+
companion object {
10+
private val ExcludedClassNameEndings by lazy {
11+
setOf("Dto", "Entity")
12+
}
13+
14+
private val ExcludedAnnotations by lazy {
15+
setOf("Entity", "Serializable")
16+
}
17+
18+
private val IdFieldEndings by lazy {
19+
setOf("Id", "ID")
20+
}
21+
}
22+
23+
override val issue = Issue(
24+
id = "DataClassTypedIDs",
25+
severity = Severity.Maintainability,
26+
description = "Domain data models should use type-safe `value class` ids. " +
27+
"Typed-IDs provide compile-time safety and prevent mixing IDs of different entities.",
28+
debt = Debt.TWENTY_MINS
29+
)
30+
31+
override fun visitClass(klass: KtClass) {
32+
super.visitClass(klass)
33+
if (klass.isData() && !klass.isIgnoredClass()) {
34+
klass.getPrimaryConstructorParameterList()
35+
?.parameters
36+
?.filter { param ->
37+
!param.isOverride() && param.seemsLikeID()
38+
}
39+
?.forEach { parameter ->
40+
report(
41+
CodeSmell(
42+
issue,
43+
Entity.from(parameter),
44+
message = failureMessage(klass, parameter)
45+
)
46+
)
47+
}
48+
}
49+
}
50+
51+
private fun KtClass.isIgnoredClass(): Boolean {
52+
name?.let { klasName ->
53+
val isIgnored = ExcludedClassNameEndings.any {
54+
klasName.endsWith(it, ignoreCase = true)
55+
}
56+
if (isIgnored) return true
57+
}
58+
59+
return annotationEntries.any {
60+
val annotationName = it.shortName?.asString()
61+
annotationName in ExcludedAnnotations
62+
}
63+
}
64+
65+
private fun KtParameter.seemsLikeID(): Boolean {
66+
val paramType = typeReference?.text
67+
if (paramType == "UUID") return true
68+
69+
name?.let { paramName ->
70+
val endsLikeID = IdFieldEndings.any {
71+
paramName.endsWith(it, ignoreCase = false)
72+
}
73+
if (endsLikeID) return true
74+
}
75+
76+
return false
77+
}
78+
79+
private fun failureMessage(klass: KtClass, parameter: KtParameter) = buildString {
80+
val paramType = parameter.typeReference?.text
81+
append("Data class '${klass.name}' should use type-safe IDs ")
82+
append("instead of $paramType for property '${parameter.name}'. ")
83+
append("Typed-IDs like `value class SomeId(val id: UUID)` provide ")
84+
append("compile-time safety and prevent mixing IDs of different entities.")
85+
}
86+
}

src/main/resources/config/config.yml

+2
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@ IvyExplicit:
33
active: true
44
DataClassDefaultValues:
55
active: true
6+
DataClassTypedIDs:
7+
active: true
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.github.ivy.explicit.rule
2+
3+
import io.gitlab.arturbosch.detekt.api.Config
4+
import io.gitlab.arturbosch.detekt.rules.KotlinCoreEnvironmentTest
5+
import io.gitlab.arturbosch.detekt.test.compileAndLintWithContext
6+
import io.kotest.matchers.collections.shouldHaveSize
7+
import io.kotest.matchers.shouldBe
8+
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
9+
import org.junit.jupiter.api.Test
10+
11+
@KotlinCoreEnvironmentTest
12+
internal class DataClassTypedIDsRuleRuleTest(private val env: KotlinCoreEnvironment) {
13+
14+
@Test
15+
fun `reports data class having UUID as id`() {
16+
val code = """
17+
data class A(
18+
val id: UUID,
19+
val name: String,
20+
)
21+
"""
22+
val findings = DataClassTypedIDsRule(Config.empty).compileAndLintWithContext(env, code)
23+
findings shouldHaveSize 1
24+
val message = findings.first().message
25+
message shouldBe """
26+
Data class 'A' should use type-safe IDs instead of UUID for property 'id'. Typed-IDs like `value class SomeId(val id: UUID)` provide compile-time safety and prevent mixing IDs of different entities.
27+
""".trimIndent()
28+
}
29+
}

0 commit comments

Comments
 (0)