From a2d1955d90cb39f104fd468a350ba67264240299 Mon Sep 17 00:00:00 2001 From: yevhen-kryvoshei Date: Fri, 29 Aug 2025 15:19:18 +0300 Subject: [PATCH 1/6] Added rules for data->domain and ui-data imports --- .../ktlint/NoDataImportInDomainRule.kt | 54 ++++++++++++++++++ .../sdkforge/ktlint/NoUiImportInDataRule.kt | 55 +++++++++++++++++++ .../ktlint/NoDataImportInDomainRuleTest.kt | 23 ++++++++ .../ktlint/NoUiImportInDataRuleTest.kt | 23 ++++++++ 4 files changed, 155 insertions(+) create mode 100644 internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/NoDataImportInDomainRule.kt create mode 100644 internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/NoUiImportInDataRule.kt create mode 100644 internal-ktlint/src/test/kotlin/dev/sdkforge/ktlint/NoDataImportInDomainRuleTest.kt create mode 100644 internal-ktlint/src/test/kotlin/dev/sdkforge/ktlint/NoUiImportInDataRuleTest.kt diff --git a/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/NoDataImportInDomainRule.kt b/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/NoDataImportInDomainRule.kt new file mode 100644 index 0000000..21ed124 --- /dev/null +++ b/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/NoDataImportInDomainRule.kt @@ -0,0 +1,54 @@ +package dev.sdkforge.ktlint + +import com.pinterest.ktlint.rule.engine.core.api.AutocorrectDecision +import com.pinterest.ktlint.rule.engine.core.api.Rule +import com.pinterest.ktlint.rule.engine.core.api.RuleAutocorrectApproveHandler +import com.pinterest.ktlint.rule.engine.core.api.RuleId +import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.psi.KtImportDirective +import org.jetbrains.kotlin.psi.KtPackageDirective +import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes + +//TODO could all 'import in package' rules be made into a single rule with an arguments coming from rule constructor? +class NoDataImportInDomainRule() : Rule( + ruleId = RuleId("$SDKFORGE_RULE_SET_ID:no-data-import-in-domain"), + about = About( + maintainer = "azazellj", + repositoryUrl = "https://github.com/SDKForge/template-sdk", + issueTrackerUrl = "https://github.com/SDKForge/template-sdk/issues", + ), +), RuleAutocorrectApproveHandler { + override fun beforeVisitChildNodes( + node: ASTNode, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> AutocorrectDecision, + ) { + when (node.elementType) { + KtStubElementTypes.PACKAGE_DIRECTIVE -> { + val packageDirective = node.psi as KtPackageDirective + lastPackageName = packageDirective.name + } + + KtStubElementTypes.IMPORT_DIRECTIVE -> { + val importDirective = node.psi as KtImportDirective + currentImportPath = importDirective.importPath?.pathStr.orEmpty() + val isDataImport = currentImportPath.contains(DATA_MODULE_NAME) + val isPackageNameValid = lastPackageName.contains(DOMAIN_MODULE_NAME) + + if (isDataImport && isPackageNameValid) { + emit(node.startOffset, RULE_VIOLATION_MESSAGE, false) + } + } + + else -> {} + } + } + + companion object { + private var lastPackageName = "" + private var currentImportPath = "" + private const val DATA_MODULE_NAME = "data" + private const val DOMAIN_MODULE_NAME = "domain" + + val RULE_VIOLATION_MESSAGE = "Importing: $currentImportPath. $DATA_MODULE_NAME module import is not allowed in $DOMAIN_MODULE_NAME module" + } +} diff --git a/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/NoUiImportInDataRule.kt b/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/NoUiImportInDataRule.kt new file mode 100644 index 0000000..9890f68 --- /dev/null +++ b/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/NoUiImportInDataRule.kt @@ -0,0 +1,55 @@ +package dev.sdkforge.ktlint + +import com.pinterest.ktlint.rule.engine.core.api.AutocorrectDecision +import com.pinterest.ktlint.rule.engine.core.api.Rule +import com.pinterest.ktlint.rule.engine.core.api.RuleAutocorrectApproveHandler +import com.pinterest.ktlint.rule.engine.core.api.RuleId +import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.psi.KtImportDirective +import org.jetbrains.kotlin.psi.KtPackageDirective +import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes + +//TODO could all 'import in package' rules be made into a single rule with an arguments coming from rule constructor? +class NoUiImportInDataRule() : Rule( + ruleId = RuleId("$SDKFORGE_RULE_SET_ID:no-ui-import-in-data"), + about = About( + maintainer = "azazellj", + repositoryUrl = "https://github.com/SDKForge/template-sdk", + issueTrackerUrl = "https://github.com/SDKForge/template-sdk/issues", + ), +), RuleAutocorrectApproveHandler { + override fun beforeVisitChildNodes( + node: ASTNode, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> AutocorrectDecision, + ) { + when (node.elementType) { + KtStubElementTypes.PACKAGE_DIRECTIVE -> { + val packageDirective = node.psi as KtPackageDirective + lastPackageName = packageDirective.name + } + + KtStubElementTypes.IMPORT_DIRECTIVE -> { + val importDirective = node.psi as KtImportDirective + currentImportPath = importDirective.importPath?.pathStr.orEmpty() + val isDataImport = currentImportPath.contains(UI_MODULE_NAME) + val isPackageNameValid = lastPackageName.contains(DATA_MODULE_NAME) + + if (isDataImport && isPackageNameValid) { + emit(node.startOffset, RULE_VIOLATION_MESSAGE, false) + } + } + + else -> {} + } + } + + companion object { + private var lastPackageName = "" + private var currentImportPath = "" + private const val UI_MODULE_NAME = "ui" + private const val DATA_MODULE_NAME = "data" + + val RULE_VIOLATION_MESSAGE = + "Importing: $currentImportPath. $UI_MODULE_NAME module import is not allowed in $DATA_MODULE_NAME module" + } +} diff --git a/internal-ktlint/src/test/kotlin/dev/sdkforge/ktlint/NoDataImportInDomainRuleTest.kt b/internal-ktlint/src/test/kotlin/dev/sdkforge/ktlint/NoDataImportInDomainRuleTest.kt new file mode 100644 index 0000000..5981466 --- /dev/null +++ b/internal-ktlint/src/test/kotlin/dev/sdkforge/ktlint/NoDataImportInDomainRuleTest.kt @@ -0,0 +1,23 @@ +package dev.sdkforge.ktlint + +import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule +import kotlin.test.Test + +class NoDataImportInDomainRuleTest { + private val wrappingRuleAssertThat = assertThatRule { NoDataImportInDomainRule() } + + @Test + fun `no 'data' import in 'domain' rule`() { + val code = + """ + package dev.sdkforge.shared.domain + + import dev.sdkforge.shared.data + """.trimIndent() + wrappingRuleAssertThat(code).hasLintViolationWithoutAutoCorrect( + 3, + 1, + NoDataImportInDomainRule.RULE_VIOLATION_MESSAGE, + ) + } +} diff --git a/internal-ktlint/src/test/kotlin/dev/sdkforge/ktlint/NoUiImportInDataRuleTest.kt b/internal-ktlint/src/test/kotlin/dev/sdkforge/ktlint/NoUiImportInDataRuleTest.kt new file mode 100644 index 0000000..5ee90d6 --- /dev/null +++ b/internal-ktlint/src/test/kotlin/dev/sdkforge/ktlint/NoUiImportInDataRuleTest.kt @@ -0,0 +1,23 @@ +package dev.sdkforge.ktlint + +import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule +import kotlin.test.Test + +class NoUiImportInDataRuleTest { + private val wrappingRuleAssertThat = assertThatRule { NoUiImportInDataRule() } + + @Test + fun `no 'ui' import in 'data' rule`() { + val code = + """ + package dev.sdkforge.shared.data + + import dev.sdkforge.shared.ui + """.trimIndent() + wrappingRuleAssertThat(code).hasLintViolationWithoutAutoCorrect( + 3, + 1, + NoUiImportInDataRule.RULE_VIOLATION_MESSAGE, + ) + } +} From 269d1ac1056f8c34f0df07aacb10bfe44dacc3b6 Mon Sep 17 00:00:00 2001 From: yevhen-kryvoshei Date: Fri, 29 Aug 2025 16:57:30 +0300 Subject: [PATCH 2/6] Added rules to a provider --- .../main/kotlin/dev/sdkforge/ktlint/SdkForgeRuleSetProvider.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/SdkForgeRuleSetProvider.kt b/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/SdkForgeRuleSetProvider.kt index 93378e3..fce7312 100644 --- a/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/SdkForgeRuleSetProvider.kt +++ b/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/SdkForgeRuleSetProvider.kt @@ -10,5 +10,7 @@ class SdkForgeRuleSetProvider : RuleSetProviderV3(RuleSetId(SDKFORGE_RULE_SET_ID override fun getRuleProviders(): Set = setOf( RuleProvider { NoTemplateImportRule() }, RuleProvider { NoTemplatePackageRule() }, + RuleProvider { NoDataImportInDomainRule() }, + RuleProvider { NoUiImportInDataRule() } ) } From 06fd06b703c0759bd94d07bd9f5499752f4de0b7 Mon Sep 17 00:00:00 2001 From: yevhen-kryvoshei Date: Fri, 29 Aug 2025 20:05:18 +0300 Subject: [PATCH 3/6] Code clean up --- .../ktlint/NoDataImportInDomainRule.kt | 21 +++++++++++-------- .../sdkforge/ktlint/NoUiImportInDataRule.kt | 18 +++++++++------- .../ktlint/SdkForgeRuleSetProvider.kt | 2 +- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/NoDataImportInDomainRule.kt b/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/NoDataImportInDomainRule.kt index 21ed124..1403c83 100644 --- a/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/NoDataImportInDomainRule.kt +++ b/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/NoDataImportInDomainRule.kt @@ -9,15 +9,17 @@ import org.jetbrains.kotlin.psi.KtImportDirective import org.jetbrains.kotlin.psi.KtPackageDirective import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes -//TODO could all 'import in package' rules be made into a single rule with an arguments coming from rule constructor? -class NoDataImportInDomainRule() : Rule( - ruleId = RuleId("$SDKFORGE_RULE_SET_ID:no-data-import-in-domain"), - about = About( - maintainer = "azazellj", - repositoryUrl = "https://github.com/SDKForge/template-sdk", - issueTrackerUrl = "https://github.com/SDKForge/template-sdk/issues", +// TODO could all 'import in package' rules be made into a single rule with an arguments coming from rule constructor? +class NoDataImportInDomainRule : + Rule( + ruleId = RuleId("$SDKFORGE_RULE_SET_ID:no-data-import-in-domain"), + about = About( + maintainer = "azazellj", + repositoryUrl = "https://github.com/SDKForge/template-sdk", + issueTrackerUrl = "https://github.com/SDKForge/template-sdk/issues", + ), ), -), RuleAutocorrectApproveHandler { + RuleAutocorrectApproveHandler { override fun beforeVisitChildNodes( node: ASTNode, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> AutocorrectDecision, @@ -49,6 +51,7 @@ class NoDataImportInDomainRule() : Rule( private const val DATA_MODULE_NAME = "data" private const val DOMAIN_MODULE_NAME = "domain" - val RULE_VIOLATION_MESSAGE = "Importing: $currentImportPath. $DATA_MODULE_NAME module import is not allowed in $DOMAIN_MODULE_NAME module" + val RULE_VIOLATION_MESSAGE = + "Importing: $currentImportPath. $DATA_MODULE_NAME module import is not allowed in $DOMAIN_MODULE_NAME module" } } diff --git a/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/NoUiImportInDataRule.kt b/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/NoUiImportInDataRule.kt index 9890f68..df41dd3 100644 --- a/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/NoUiImportInDataRule.kt +++ b/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/NoUiImportInDataRule.kt @@ -9,15 +9,17 @@ import org.jetbrains.kotlin.psi.KtImportDirective import org.jetbrains.kotlin.psi.KtPackageDirective import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes -//TODO could all 'import in package' rules be made into a single rule with an arguments coming from rule constructor? -class NoUiImportInDataRule() : Rule( - ruleId = RuleId("$SDKFORGE_RULE_SET_ID:no-ui-import-in-data"), - about = About( - maintainer = "azazellj", - repositoryUrl = "https://github.com/SDKForge/template-sdk", - issueTrackerUrl = "https://github.com/SDKForge/template-sdk/issues", +// TODO could all 'import in package' rules be made into a single rule with an arguments coming from rule constructor? +class NoUiImportInDataRule : + Rule( + ruleId = RuleId("$SDKFORGE_RULE_SET_ID:no-ui-import-in-data"), + about = About( + maintainer = "azazellj", + repositoryUrl = "https://github.com/SDKForge/template-sdk", + issueTrackerUrl = "https://github.com/SDKForge/template-sdk/issues", + ), ), -), RuleAutocorrectApproveHandler { + RuleAutocorrectApproveHandler { override fun beforeVisitChildNodes( node: ASTNode, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> AutocorrectDecision, diff --git a/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/SdkForgeRuleSetProvider.kt b/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/SdkForgeRuleSetProvider.kt index fce7312..c7513b3 100644 --- a/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/SdkForgeRuleSetProvider.kt +++ b/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/SdkForgeRuleSetProvider.kt @@ -11,6 +11,6 @@ class SdkForgeRuleSetProvider : RuleSetProviderV3(RuleSetId(SDKFORGE_RULE_SET_ID RuleProvider { NoTemplateImportRule() }, RuleProvider { NoTemplatePackageRule() }, RuleProvider { NoDataImportInDomainRule() }, - RuleProvider { NoUiImportInDataRule() } + RuleProvider { NoUiImportInDataRule() }, ) } From 48d22bb1079e1d40c2d8605bf9a78cda403a5fd3 Mon Sep 17 00:00:00 2001 From: yevhen-kryvoshei Date: Mon, 1 Sep 2025 13:15:43 +0300 Subject: [PATCH 4/6] #7: ktlint rules for proper structure model imports - added rule for import to package import condition check --- .../ktlint/NoImportAllowedInPackageRule.kt | 71 ++++++++++++++++ .../ktlint/SdkForgeRuleSetProvider.kt | 17 +++- .../NoImportAllowedInPackageRuleTest.kt | 83 +++++++++++++++++++ 3 files changed, 169 insertions(+), 2 deletions(-) create mode 100644 internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/NoImportAllowedInPackageRule.kt create mode 100644 internal-ktlint/src/test/kotlin/dev/sdkforge/ktlint/NoImportAllowedInPackageRuleTest.kt diff --git a/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/NoImportAllowedInPackageRule.kt b/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/NoImportAllowedInPackageRule.kt new file mode 100644 index 0000000..ce3e20c --- /dev/null +++ b/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/NoImportAllowedInPackageRule.kt @@ -0,0 +1,71 @@ +package dev.sdkforge.ktlint + +import com.pinterest.ktlint.rule.engine.core.api.AutocorrectDecision +import com.pinterest.ktlint.rule.engine.core.api.Rule +import com.pinterest.ktlint.rule.engine.core.api.RuleAutocorrectApproveHandler +import com.pinterest.ktlint.rule.engine.core.api.RuleId +import kotlin.text.isNullOrEmpty +import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.psi.KtImportDirective +import org.jetbrains.kotlin.psi.KtPackageDirective +import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes + +class NoImportAllowedInPackageRule(targetPackages: Pair) : + Rule( + ruleId = RuleId("$SDKFORGE_RULE_SET_ID:no-import-allowed-in-package"), + about = About( + maintainer = "azazellj", + repositoryUrl = "https://github.com/SDKForge/template-sdk", + issueTrackerUrl = "https://github.com/SDKForge/template-sdk/issues", + ), + ), + RuleAutocorrectApproveHandler { + private lateinit var modulePackageName: String + private val imports = mutableSetOf() + private val targetImportName = targetPackages.first + private val targetPackageName = targetPackages.second + + override fun beforeVisitChildNodes( + node: ASTNode, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> AutocorrectDecision, + ) { + when (node.elementType) { + KtStubElementTypes.PACKAGE_DIRECTIVE -> { + val packageDirective = node.psi as KtPackageDirective + modulePackageName = packageDirective.qualifiedName + } + KtStubElementTypes.IMPORT_DIRECTIVE -> { + val importDirective = node.psi as KtImportDirective + val isNameValid = !importDirective.importPath?.pathStr.isNullOrEmpty() + if (isNameValid) { + imports.add(importDirective) + } + } + else -> { + } + } + } + + override fun afterVisitChildNodes( + node: ASTNode, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> AutocorrectDecision, + ) { + if (node.elementType != KtStubElementTypes.IMPORT_LIST) return + val isReadyToCompareImports = imports.isNotEmpty() && modulePackageName.contains(targetPackageName) + if (isReadyToCompareImports) { + for (import in imports) { + val importName = import.importPath?.pathStr.orEmpty() + val isTargetImport = importName.contains(targetImportName) + if (isTargetImport) { + emit(node.startOffset, RULE_VIOLATION_MESSAGE.format(importName, targetImportName, targetPackageName), false) + return + } + } + } + } + + companion object { + private const val RULE_VIOLATION_MESSAGE = + "Importing: %1s. %2s module import is not allowed in %3s module" + } +} diff --git a/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/SdkForgeRuleSetProvider.kt b/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/SdkForgeRuleSetProvider.kt index c7513b3..dba2662 100644 --- a/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/SdkForgeRuleSetProvider.kt +++ b/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/SdkForgeRuleSetProvider.kt @@ -7,10 +7,23 @@ import com.pinterest.ktlint.rule.engine.core.api.RuleSetId internal const val SDKFORGE_RULE_SET_ID = "sdkforge-ktlint-rules" class SdkForgeRuleSetProvider : RuleSetProviderV3(RuleSetId(SDKFORGE_RULE_SET_ID)) { + private val forbiddenPackagesCombinations = mapOf( + "domain" to listOf("data", "ui"), + "data" to listOf("ui", "presentation"), + ) override fun getRuleProviders(): Set = setOf( RuleProvider { NoTemplateImportRule() }, RuleProvider { NoTemplatePackageRule() }, - RuleProvider { NoDataImportInDomainRule() }, - RuleProvider { NoUiImportInDataRule() }, + *getForbiddenPackagesCombinationsRules().toTypedArray(), ) + + private fun getForbiddenPackagesCombinationsRules(): MutableSet { + val combinationSet = mutableSetOf() + for (packageName in forbiddenPackagesCombinations) { + for (importName in packageName.value) { + combinationSet.add(RuleProvider { NoImportAllowedInPackageRule(importName to packageName.key) }) + } + } + return combinationSet + } } diff --git a/internal-ktlint/src/test/kotlin/dev/sdkforge/ktlint/NoImportAllowedInPackageRuleTest.kt b/internal-ktlint/src/test/kotlin/dev/sdkforge/ktlint/NoImportAllowedInPackageRuleTest.kt new file mode 100644 index 0000000..f185fd3 --- /dev/null +++ b/internal-ktlint/src/test/kotlin/dev/sdkforge/ktlint/NoImportAllowedInPackageRuleTest.kt @@ -0,0 +1,83 @@ +package dev.sdkforge.ktlint + +import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule +import kotlin.test.Test + +class NoImportAllowedInPackageRuleTest { + + @Test + fun `no 'data' import in 'domain' rule`() { + val assertThatDataInDomainRule = assertThatRule { NoImportAllowedInPackageRule("data" to "domain") } + val code = + """ + package dev.sdkforge.shared.domain + + import dev.sdkforge.shared.data + import dev.sdkforge.shared.ui + import dev.sdkforge.shared.presentation + """.trimIndent() + val expectedMessage = "Importing: dev.sdkforge.shared.data. data module import is not allowed in domain module" + assertThatDataInDomainRule(code).hasLintViolationWithoutAutoCorrect( + 3, + 1, + expectedMessage, + ) + } + + @Test + fun `no 'ui' import in 'domain' rule`() { + val assertThatUiInDomainRule = assertThatRule { NoImportAllowedInPackageRule("ui" to "domain") } + val code = + """ + package dev.sdkforge.shared.domain + + import dev.sdkforge.shared.ui + import dev.sdkforge.shared.data + import dev.sdkforge.shared.presentation + """.trimIndent() + val expectedMessage = "Importing: dev.sdkforge.shared.ui. ui module import is not allowed in domain module" + assertThatUiInDomainRule(code).hasLintViolationWithoutAutoCorrect( + 3, + 1, + expectedMessage, + ) + } + + @Test + fun `no 'ui' import in 'data' rule`() { + val assertThatUiDataRule = assertThatRule { NoImportAllowedInPackageRule("ui" to "data") } + val code = + """ + package dev.sdkforge.shared.data + + import dev.sdkforge.shared.ui + import dev.sdkforge.shared.presentation + import dev.sdkforge.shared.domain + """.trimIndent() + val expectedMessage = "Importing: dev.sdkforge.shared.ui. ui module import is not allowed in data module" + assertThatUiDataRule(code).hasLintViolationWithoutAutoCorrect( + 3, + 1, + expectedMessage, + ) + } + + @Test + fun `no 'presentation' import in 'data' rule`() { + val assertThatPresentationDataRule = assertThatRule { NoImportAllowedInPackageRule("presentation" to "data") } + val code = + """ + package dev.sdkforge.shared.data + + import dev.sdkforge.shared.presentation + import dev.sdkforge.shared.ui + import dev.sdkforge.shared.domain + """.trimIndent() + val expectedMessage = "Importing: dev.sdkforge.shared.presentation. presentation module import is not allowed in data module" + assertThatPresentationDataRule(code).hasLintViolationWithoutAutoCorrect( + 3, + 1, + expectedMessage, + ) + } +} From fefbcf04725af3494b83ceb950dc53379ef25403 Mon Sep 17 00:00:00 2001 From: yevhen-kryvoshei Date: Mon, 1 Sep 2025 13:18:43 +0300 Subject: [PATCH 5/6] #7: ktlint rules for proper structure model imports - removed redundant rules and tests --- .../ktlint/NoDataImportInDomainRule.kt | 57 ------------------- .../sdkforge/ktlint/NoUiImportInDataRule.kt | 57 ------------------- .../ktlint/NoDataImportInDomainRuleTest.kt | 23 -------- .../ktlint/NoUiImportInDataRuleTest.kt | 23 -------- 4 files changed, 160 deletions(-) delete mode 100644 internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/NoDataImportInDomainRule.kt delete mode 100644 internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/NoUiImportInDataRule.kt delete mode 100644 internal-ktlint/src/test/kotlin/dev/sdkforge/ktlint/NoDataImportInDomainRuleTest.kt delete mode 100644 internal-ktlint/src/test/kotlin/dev/sdkforge/ktlint/NoUiImportInDataRuleTest.kt diff --git a/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/NoDataImportInDomainRule.kt b/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/NoDataImportInDomainRule.kt deleted file mode 100644 index 1403c83..0000000 --- a/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/NoDataImportInDomainRule.kt +++ /dev/null @@ -1,57 +0,0 @@ -package dev.sdkforge.ktlint - -import com.pinterest.ktlint.rule.engine.core.api.AutocorrectDecision -import com.pinterest.ktlint.rule.engine.core.api.Rule -import com.pinterest.ktlint.rule.engine.core.api.RuleAutocorrectApproveHandler -import com.pinterest.ktlint.rule.engine.core.api.RuleId -import org.jetbrains.kotlin.com.intellij.lang.ASTNode -import org.jetbrains.kotlin.psi.KtImportDirective -import org.jetbrains.kotlin.psi.KtPackageDirective -import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes - -// TODO could all 'import in package' rules be made into a single rule with an arguments coming from rule constructor? -class NoDataImportInDomainRule : - Rule( - ruleId = RuleId("$SDKFORGE_RULE_SET_ID:no-data-import-in-domain"), - about = About( - maintainer = "azazellj", - repositoryUrl = "https://github.com/SDKForge/template-sdk", - issueTrackerUrl = "https://github.com/SDKForge/template-sdk/issues", - ), - ), - RuleAutocorrectApproveHandler { - override fun beforeVisitChildNodes( - node: ASTNode, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> AutocorrectDecision, - ) { - when (node.elementType) { - KtStubElementTypes.PACKAGE_DIRECTIVE -> { - val packageDirective = node.psi as KtPackageDirective - lastPackageName = packageDirective.name - } - - KtStubElementTypes.IMPORT_DIRECTIVE -> { - val importDirective = node.psi as KtImportDirective - currentImportPath = importDirective.importPath?.pathStr.orEmpty() - val isDataImport = currentImportPath.contains(DATA_MODULE_NAME) - val isPackageNameValid = lastPackageName.contains(DOMAIN_MODULE_NAME) - - if (isDataImport && isPackageNameValid) { - emit(node.startOffset, RULE_VIOLATION_MESSAGE, false) - } - } - - else -> {} - } - } - - companion object { - private var lastPackageName = "" - private var currentImportPath = "" - private const val DATA_MODULE_NAME = "data" - private const val DOMAIN_MODULE_NAME = "domain" - - val RULE_VIOLATION_MESSAGE = - "Importing: $currentImportPath. $DATA_MODULE_NAME module import is not allowed in $DOMAIN_MODULE_NAME module" - } -} diff --git a/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/NoUiImportInDataRule.kt b/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/NoUiImportInDataRule.kt deleted file mode 100644 index df41dd3..0000000 --- a/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/NoUiImportInDataRule.kt +++ /dev/null @@ -1,57 +0,0 @@ -package dev.sdkforge.ktlint - -import com.pinterest.ktlint.rule.engine.core.api.AutocorrectDecision -import com.pinterest.ktlint.rule.engine.core.api.Rule -import com.pinterest.ktlint.rule.engine.core.api.RuleAutocorrectApproveHandler -import com.pinterest.ktlint.rule.engine.core.api.RuleId -import org.jetbrains.kotlin.com.intellij.lang.ASTNode -import org.jetbrains.kotlin.psi.KtImportDirective -import org.jetbrains.kotlin.psi.KtPackageDirective -import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes - -// TODO could all 'import in package' rules be made into a single rule with an arguments coming from rule constructor? -class NoUiImportInDataRule : - Rule( - ruleId = RuleId("$SDKFORGE_RULE_SET_ID:no-ui-import-in-data"), - about = About( - maintainer = "azazellj", - repositoryUrl = "https://github.com/SDKForge/template-sdk", - issueTrackerUrl = "https://github.com/SDKForge/template-sdk/issues", - ), - ), - RuleAutocorrectApproveHandler { - override fun beforeVisitChildNodes( - node: ASTNode, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> AutocorrectDecision, - ) { - when (node.elementType) { - KtStubElementTypes.PACKAGE_DIRECTIVE -> { - val packageDirective = node.psi as KtPackageDirective - lastPackageName = packageDirective.name - } - - KtStubElementTypes.IMPORT_DIRECTIVE -> { - val importDirective = node.psi as KtImportDirective - currentImportPath = importDirective.importPath?.pathStr.orEmpty() - val isDataImport = currentImportPath.contains(UI_MODULE_NAME) - val isPackageNameValid = lastPackageName.contains(DATA_MODULE_NAME) - - if (isDataImport && isPackageNameValid) { - emit(node.startOffset, RULE_VIOLATION_MESSAGE, false) - } - } - - else -> {} - } - } - - companion object { - private var lastPackageName = "" - private var currentImportPath = "" - private const val UI_MODULE_NAME = "ui" - private const val DATA_MODULE_NAME = "data" - - val RULE_VIOLATION_MESSAGE = - "Importing: $currentImportPath. $UI_MODULE_NAME module import is not allowed in $DATA_MODULE_NAME module" - } -} diff --git a/internal-ktlint/src/test/kotlin/dev/sdkforge/ktlint/NoDataImportInDomainRuleTest.kt b/internal-ktlint/src/test/kotlin/dev/sdkforge/ktlint/NoDataImportInDomainRuleTest.kt deleted file mode 100644 index 5981466..0000000 --- a/internal-ktlint/src/test/kotlin/dev/sdkforge/ktlint/NoDataImportInDomainRuleTest.kt +++ /dev/null @@ -1,23 +0,0 @@ -package dev.sdkforge.ktlint - -import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule -import kotlin.test.Test - -class NoDataImportInDomainRuleTest { - private val wrappingRuleAssertThat = assertThatRule { NoDataImportInDomainRule() } - - @Test - fun `no 'data' import in 'domain' rule`() { - val code = - """ - package dev.sdkforge.shared.domain - - import dev.sdkforge.shared.data - """.trimIndent() - wrappingRuleAssertThat(code).hasLintViolationWithoutAutoCorrect( - 3, - 1, - NoDataImportInDomainRule.RULE_VIOLATION_MESSAGE, - ) - } -} diff --git a/internal-ktlint/src/test/kotlin/dev/sdkforge/ktlint/NoUiImportInDataRuleTest.kt b/internal-ktlint/src/test/kotlin/dev/sdkforge/ktlint/NoUiImportInDataRuleTest.kt deleted file mode 100644 index 5ee90d6..0000000 --- a/internal-ktlint/src/test/kotlin/dev/sdkforge/ktlint/NoUiImportInDataRuleTest.kt +++ /dev/null @@ -1,23 +0,0 @@ -package dev.sdkforge.ktlint - -import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule -import kotlin.test.Test - -class NoUiImportInDataRuleTest { - private val wrappingRuleAssertThat = assertThatRule { NoUiImportInDataRule() } - - @Test - fun `no 'ui' import in 'data' rule`() { - val code = - """ - package dev.sdkforge.shared.data - - import dev.sdkforge.shared.ui - """.trimIndent() - wrappingRuleAssertThat(code).hasLintViolationWithoutAutoCorrect( - 3, - 1, - NoUiImportInDataRule.RULE_VIOLATION_MESSAGE, - ) - } -} From 0bee9cf9488e0d4829ab189f66082f850b5b067f Mon Sep 17 00:00:00 2001 From: azazellj Date: Mon, 1 Sep 2025 18:37:27 +0300 Subject: [PATCH 6/6] #7: ktlint rules for proper structure model imports --- .../ktlint/NoImportAllowedInPackageRule.kt | 48 +++++--------- .../ktlint/SdkForgeRuleSetProvider.kt | 20 +++--- .../NoImportAllowedInPackageRuleTest.kt | 63 ++++--------------- 3 files changed, 35 insertions(+), 96 deletions(-) diff --git a/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/NoImportAllowedInPackageRule.kt b/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/NoImportAllowedInPackageRule.kt index ce3e20c..1619872 100644 --- a/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/NoImportAllowedInPackageRule.kt +++ b/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/NoImportAllowedInPackageRule.kt @@ -4,13 +4,12 @@ import com.pinterest.ktlint.rule.engine.core.api.AutocorrectDecision import com.pinterest.ktlint.rule.engine.core.api.Rule import com.pinterest.ktlint.rule.engine.core.api.RuleAutocorrectApproveHandler import com.pinterest.ktlint.rule.engine.core.api.RuleId -import kotlin.text.isNullOrEmpty import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.psi.KtImportDirective import org.jetbrains.kotlin.psi.KtPackageDirective import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes -class NoImportAllowedInPackageRule(targetPackages: Pair) : +class NoImportAllowedInPackageRule(private val forbiddenPackageImports: Map>) : Rule( ruleId = RuleId("$SDKFORGE_RULE_SET_ID:no-import-allowed-in-package"), about = About( @@ -20,29 +19,17 @@ class NoImportAllowedInPackageRule(targetPackages: Pair) : ), ), RuleAutocorrectApproveHandler { - private lateinit var modulePackageName: String - private val imports = mutableSetOf() - private val targetImportName = targetPackages.first - private val targetPackageName = targetPackages.second + private lateinit var filePackage: KtPackageDirective + private val fileImports = mutableSetOf() override fun beforeVisitChildNodes( node: ASTNode, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> AutocorrectDecision, ) { when (node.elementType) { - KtStubElementTypes.PACKAGE_DIRECTIVE -> { - val packageDirective = node.psi as KtPackageDirective - modulePackageName = packageDirective.qualifiedName - } - KtStubElementTypes.IMPORT_DIRECTIVE -> { - val importDirective = node.psi as KtImportDirective - val isNameValid = !importDirective.importPath?.pathStr.isNullOrEmpty() - if (isNameValid) { - imports.add(importDirective) - } - } - else -> { - } + KtStubElementTypes.PACKAGE_DIRECTIVE -> filePackage = node.psi as KtPackageDirective + KtStubElementTypes.IMPORT_DIRECTIVE -> fileImports += node + else -> Unit } } @@ -50,22 +37,21 @@ class NoImportAllowedInPackageRule(targetPackages: Pair) : node: ASTNode, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> AutocorrectDecision, ) { + if (!::filePackage.isInitialized) return if (node.elementType != KtStubElementTypes.IMPORT_LIST) return - val isReadyToCompareImports = imports.isNotEmpty() && modulePackageName.contains(targetPackageName) - if (isReadyToCompareImports) { - for (import in imports) { - val importName = import.importPath?.pathStr.orEmpty() - val isTargetImport = importName.contains(targetImportName) - if (isTargetImport) { - emit(node.startOffset, RULE_VIOLATION_MESSAGE.format(importName, targetImportName, targetPackageName), false) - return - } - } + + val forbiddenImports = forbiddenPackageImports.entries.find { filePackage.qualifiedName.contains(it.key) }?.value ?: return + + for (fileImport in fileImports) { + val importName = (fileImport.psi as KtImportDirective).importPath?.pathStr.orEmpty() + + if (forbiddenImports.none { fileImport.text.contains(it) }) continue + + emit(fileImport.startOffset, RULE_VIOLATION_MESSAGE.format(importName, filePackage.qualifiedName), false) } } companion object { - private const val RULE_VIOLATION_MESSAGE = - "Importing: %1s. %2s module import is not allowed in %3s module" + private const val RULE_VIOLATION_MESSAGE = "Importing '%1s', it is not allowed in '%2s'" } } diff --git a/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/SdkForgeRuleSetProvider.kt b/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/SdkForgeRuleSetProvider.kt index dba2662..d75a7a2 100644 --- a/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/SdkForgeRuleSetProvider.kt +++ b/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/SdkForgeRuleSetProvider.kt @@ -7,23 +7,17 @@ import com.pinterest.ktlint.rule.engine.core.api.RuleSetId internal const val SDKFORGE_RULE_SET_ID = "sdkforge-ktlint-rules" class SdkForgeRuleSetProvider : RuleSetProviderV3(RuleSetId(SDKFORGE_RULE_SET_ID)) { - private val forbiddenPackagesCombinations = mapOf( - "domain" to listOf("data", "ui"), - "data" to listOf("ui", "presentation"), - ) + override fun getRuleProviders(): Set = setOf( RuleProvider { NoTemplateImportRule() }, RuleProvider { NoTemplatePackageRule() }, - *getForbiddenPackagesCombinationsRules().toTypedArray(), + RuleProvider { NoImportAllowedInPackageRule(FORBIDDEN_PACKAGE_IMPORTS) }, ) - private fun getForbiddenPackagesCombinationsRules(): MutableSet { - val combinationSet = mutableSetOf() - for (packageName in forbiddenPackagesCombinations) { - for (importName in packageName.value) { - combinationSet.add(RuleProvider { NoImportAllowedInPackageRule(importName to packageName.key) }) - } - } - return combinationSet + companion object { + private val FORBIDDEN_PACKAGE_IMPORTS = mapOf( + "domain" to setOf("data", "ui"), + "data" to setOf("ui", "presentation"), + ) } } diff --git a/internal-ktlint/src/test/kotlin/dev/sdkforge/ktlint/NoImportAllowedInPackageRuleTest.kt b/internal-ktlint/src/test/kotlin/dev/sdkforge/ktlint/NoImportAllowedInPackageRuleTest.kt index f185fd3..f55574a 100644 --- a/internal-ktlint/src/test/kotlin/dev/sdkforge/ktlint/NoImportAllowedInPackageRuleTest.kt +++ b/internal-ktlint/src/test/kotlin/dev/sdkforge/ktlint/NoImportAllowedInPackageRuleTest.kt @@ -1,13 +1,14 @@ package dev.sdkforge.ktlint import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule +import com.pinterest.ktlint.test.LintViolation import kotlin.test.Test class NoImportAllowedInPackageRuleTest { @Test - fun `no 'data' import in 'domain' rule`() { - val assertThatDataInDomainRule = assertThatRule { NoImportAllowedInPackageRule("data" to "domain") } + fun `no 'data', 'ui' import in 'domain' rule`() { + val assertThatDataInDomainRule = assertThatRule { NoImportAllowedInPackageRule(mapOf("domain" to setOf("data", "ui"))) } val code = """ package dev.sdkforge.shared.domain @@ -16,36 +17,15 @@ class NoImportAllowedInPackageRuleTest { import dev.sdkforge.shared.ui import dev.sdkforge.shared.presentation """.trimIndent() - val expectedMessage = "Importing: dev.sdkforge.shared.data. data module import is not allowed in domain module" - assertThatDataInDomainRule(code).hasLintViolationWithoutAutoCorrect( - 3, - 1, - expectedMessage, + assertThatDataInDomainRule(code).hasLintViolationsWithoutAutoCorrect( + LintViolation(3, 1, "Importing 'dev.sdkforge.shared.data', it is not allowed in 'dev.sdkforge.shared.domain'", false), + LintViolation(4, 1, "Importing 'dev.sdkforge.shared.ui', it is not allowed in 'dev.sdkforge.shared.domain'", false), ) } @Test - fun `no 'ui' import in 'domain' rule`() { - val assertThatUiInDomainRule = assertThatRule { NoImportAllowedInPackageRule("ui" to "domain") } - val code = - """ - package dev.sdkforge.shared.domain - - import dev.sdkforge.shared.ui - import dev.sdkforge.shared.data - import dev.sdkforge.shared.presentation - """.trimIndent() - val expectedMessage = "Importing: dev.sdkforge.shared.ui. ui module import is not allowed in domain module" - assertThatUiInDomainRule(code).hasLintViolationWithoutAutoCorrect( - 3, - 1, - expectedMessage, - ) - } - - @Test - fun `no 'ui' import in 'data' rule`() { - val assertThatUiDataRule = assertThatRule { NoImportAllowedInPackageRule("ui" to "data") } + fun `no 'ui', 'presentation' import in 'data' rule`() { + val assertThatUiDataRule = assertThatRule { NoImportAllowedInPackageRule(mapOf("data" to setOf("ui", "presentation"))) } val code = """ package dev.sdkforge.shared.data @@ -54,30 +34,9 @@ class NoImportAllowedInPackageRuleTest { import dev.sdkforge.shared.presentation import dev.sdkforge.shared.domain """.trimIndent() - val expectedMessage = "Importing: dev.sdkforge.shared.ui. ui module import is not allowed in data module" - assertThatUiDataRule(code).hasLintViolationWithoutAutoCorrect( - 3, - 1, - expectedMessage, - ) - } - - @Test - fun `no 'presentation' import in 'data' rule`() { - val assertThatPresentationDataRule = assertThatRule { NoImportAllowedInPackageRule("presentation" to "data") } - val code = - """ - package dev.sdkforge.shared.data - - import dev.sdkforge.shared.presentation - import dev.sdkforge.shared.ui - import dev.sdkforge.shared.domain - """.trimIndent() - val expectedMessage = "Importing: dev.sdkforge.shared.presentation. presentation module import is not allowed in data module" - assertThatPresentationDataRule(code).hasLintViolationWithoutAutoCorrect( - 3, - 1, - expectedMessage, + assertThatUiDataRule(code).hasLintViolationsWithoutAutoCorrect( + LintViolation(3, 1, "Importing 'dev.sdkforge.shared.ui', it is not allowed in 'dev.sdkforge.shared.data'", false), + LintViolation(4, 1, "Importing 'dev.sdkforge.shared.presentation', it is not allowed in 'dev.sdkforge.shared.data'", false), ) } }