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..1619872 --- /dev/null +++ b/internal-ktlint/src/main/kotlin/dev/sdkforge/ktlint/NoImportAllowedInPackageRule.kt @@ -0,0 +1,57 @@ +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 + +class NoImportAllowedInPackageRule(private val forbiddenPackageImports: Map>) : + 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 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 -> filePackage = node.psi as KtPackageDirective + KtStubElementTypes.IMPORT_DIRECTIVE -> fileImports += node + else -> Unit + } + } + + override fun afterVisitChildNodes( + node: ASTNode, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> AutocorrectDecision, + ) { + if (!::filePackage.isInitialized) return + if (node.elementType != KtStubElementTypes.IMPORT_LIST) 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', 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 93378e3..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,8 +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)) { + override fun getRuleProviders(): Set = setOf( RuleProvider { NoTemplateImportRule() }, RuleProvider { NoTemplatePackageRule() }, + RuleProvider { NoImportAllowedInPackageRule(FORBIDDEN_PACKAGE_IMPORTS) }, ) + + 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 new file mode 100644 index 0000000..f55574a --- /dev/null +++ b/internal-ktlint/src/test/kotlin/dev/sdkforge/ktlint/NoImportAllowedInPackageRuleTest.kt @@ -0,0 +1,42 @@ +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', 'ui' import in 'domain' rule`() { + val assertThatDataInDomainRule = assertThatRule { NoImportAllowedInPackageRule(mapOf("domain" to setOf("data", "ui"))) } + val code = + """ + package dev.sdkforge.shared.domain + + import dev.sdkforge.shared.data + import dev.sdkforge.shared.ui + import dev.sdkforge.shared.presentation + """.trimIndent() + 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', 'presentation' import in 'data' rule`() { + val assertThatUiDataRule = assertThatRule { NoImportAllowedInPackageRule(mapOf("data" to setOf("ui", "presentation"))) } + val code = + """ + package dev.sdkforge.shared.data + + import dev.sdkforge.shared.ui + import dev.sdkforge.shared.presentation + import dev.sdkforge.shared.domain + """.trimIndent() + 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), + ) + } +}