Skip to content

Commit

Permalink
Add "DataClassTypedIDs" rule
Browse files Browse the repository at this point in the history
  • Loading branch information
ILIYANGERMANOV committed Jan 30, 2024
1 parent 6096461 commit 54710a8
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.github.ivy.explicit

import com.github.ivy.explicit.rule.DataClassDefaultValuesRule
import com.github.ivy.explicit.rule.DataClassFunctionsRule
import com.github.ivy.explicit.rule.DataClassTypedIDsRule
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.RuleSet
import io.gitlab.arturbosch.detekt.api.RuleSetProvider
Expand All @@ -15,6 +16,7 @@ class IvyExplicitRuleSetProvider : RuleSetProvider {
listOf(
DataClassFunctionsRule(config),
DataClassDefaultValuesRule(config),
DataClassTypedIDsRule(config)
),
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.github.ivy.explicit.rule

import io.gitlab.arturbosch.detekt.api.*
import io.gitlab.arturbosch.detekt.rules.isOverride
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtParameter

class DataClassTypedIDsRule(config: Config) : Rule(config) {
companion object {
private val ExcludedClassNameEndings by lazy {
setOf("Dto", "Entity")
}

private val ExcludedAnnotations by lazy {
setOf("Entity", "Serializable")
}

private val IdFieldEndings by lazy {
setOf("Id", "ID")
}
}

override val issue = Issue(
id = "DataClassTypedIDs",
severity = Severity.Maintainability,
description = "Domain data models should use type-safe `value class` ids. " +
"Typed-IDs provide compile-time safety and prevent mixing IDs of different entities.",
debt = Debt.TWENTY_MINS
)

override fun visitClass(klass: KtClass) {
super.visitClass(klass)
if (klass.isData() && !klass.isIgnoredClass()) {
klass.getPrimaryConstructorParameterList()
?.parameters
?.filter { param ->
!param.isOverride() && param.seemsLikeID()
}
?.forEach { parameter ->
report(
CodeSmell(
issue,
Entity.from(parameter),
message = failureMessage(klass, parameter)
)
)
}
}
}

private fun KtClass.isIgnoredClass(): Boolean {
name?.let { klasName ->
val isIgnored = ExcludedClassNameEndings.any {
klasName.endsWith(it, ignoreCase = true)
}
if (isIgnored) return true
}

return annotationEntries.any {
val annotationName = it.shortName?.asString()
annotationName in ExcludedAnnotations
}
}

private fun KtParameter.seemsLikeID(): Boolean {
val paramType = typeReference?.text
if (paramType == "UUID") return true

name?.let { paramName ->
val endsLikeID = IdFieldEndings.any {
paramName.endsWith(it, ignoreCase = false)
}
if (endsLikeID) return true
}

return false
}

private fun failureMessage(klass: KtClass, parameter: KtParameter) = buildString {
val paramType = parameter.typeReference?.text
append("Data class '${klass.name}' should use type-safe IDs ")
append("instead of $paramType for property '${parameter.name}'. ")
append("Typed-IDs like `value class SomeId(val id: UUID)` provide ")
append("compile-time safety and prevent mixing IDs of different entities.")
}
}
2 changes: 2 additions & 0 deletions src/main/resources/config/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ IvyExplicit:
active: true
DataClassDefaultValues:
active: true
DataClassTypedIDs:
active: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.github.ivy.explicit.rule

import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.rules.KotlinCoreEnvironmentTest
import io.gitlab.arturbosch.detekt.test.compileAndLintWithContext
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.shouldBe
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.junit.jupiter.api.Test

@KotlinCoreEnvironmentTest
internal class DataClassTypedIDsRuleRuleTest(private val env: KotlinCoreEnvironment) {

@Test
fun `reports data class having UUID as id`() {
val code = """
data class A(
val id: UUID,
val name: String,
)
"""
val findings = DataClassTypedIDsRule(Config.empty).compileAndLintWithContext(env, code)
findings shouldHaveSize 1
val message = findings.first().message
message shouldBe """
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.
""".trimIndent()
}
}

0 comments on commit 54710a8

Please sign in to comment.