This commit is contained in:
2025-08-18 08:55:39 +03:00
parent ded957517a
commit 7e2e6009f7
43 changed files with 2623 additions and 1184 deletions

View File

@@ -1,4 +1,16 @@
// Путь: data/semantic-ktlint-rules/src/main/java/com/busya/ktlint/rules/CustomRuleSetProvider.kt
package com.busya.ktlint.rules
class CustomRuleSetProvider {
import com.pinterest.ktlint.rule.engine.core.api.RuleProvider
import com.pinterest.ktlint.rule.engine.core.api.RuleSetId
import com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3
class CustomRuleSetProvider : RuleSetProviderV3(RuleSetId("custom")) {
override fun getRuleProviders(): Set<RuleProvider> {
return setOf(
RuleProvider { FileHeaderRule() },
RuleProvider { MandatoryEntityDeclarationRule() },
RuleProvider { NoStrayCommentsRule() }
)
}
}

View File

@@ -1,4 +1,33 @@
// Путь: data/semantic-ktlint-rules/src/main/java/com/busya/ktlint/rules/FileHeaderRule.kt
package com.busya.ktlint.rules
class FileHeaderRule {
import com.pinterest.ktlint.rule.engine.core.api.ElementType
import com.pinterest.ktlint.rule.engine.core.api.Rule
import com.pinterest.ktlint.rule.engine.core.api.Rule.About
import com.pinterest.ktlint.rule.engine.core.api.RuleId
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
class FileHeaderRule : Rule(ruleId = RuleId("custom:file-header-rule"), about = About()) {
override fun beforeVisitChildNodes(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
) {
if (node.elementType == ElementType.FILE) {
val lines = node.text.lines()
if (lines.size < 3) {
emit(node.startOffset, "File must start with a 3-line semantic header.", false)
return
}
if (!lines[0].startsWith("// [PACKAGE]")) {
emit(node.startOffset, "File header missing or incorrect. Line 1 must be '// [PACKAGE] ...'.", false)
}
if (!lines[1].startsWith("// [FILE]")) {
emit(node.startOffset + lines[0].length + 1, "File header missing or incorrect. Line 2 must be '// [FILE] ...'.", false)
}
if (!lines[2].startsWith("// [SEMANTICS]")) {
emit(node.startOffset + lines[0].length + lines[1].length + 2, "File header missing or incorrect. Line 3 must be '// [SEMANTICS] ...'.", false)
}
}
}
}

View File

@@ -1,4 +1,40 @@
// Путь: data/semantic-ktlint-rules/src/main/java/com/busya/ktlint/rules/MandatoryEntityDeclarationRule.kt
package com.busya.ktlint.rules
class MandatoryEntityDeclarationRule {
import com.pinterest.ktlint.rule.engine.core.api.ElementType
import com.pinterest.ktlint.rule.engine.core.api.Rule
import com.pinterest.ktlint.rule.engine.core.api.Rule.About
import com.pinterest.ktlint.rule.engine.core.api.RuleId
import com.pinterest.ktlint.rule.engine.core.api.prevLeaf
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtDeclaration
class MandatoryEntityDeclarationRule : Rule(ruleId = RuleId("custom:entity-declaration-rule"), about = About()) {
private val entityTypes = setOf(
ElementType.CLASS,
ElementType.OBJECT_DECLARATION,
ElementType.FUN
)
override fun beforeVisitChildNodes(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
) {
if (node.elementType in entityTypes) {
val ktDeclaration = node.psi as? KtDeclaration ?: return
if (node.elementType == ElementType.FUN &&
(ktDeclaration.hasModifier(KtTokens.PRIVATE_KEYWORD) ||
ktDeclaration.hasModifier(KtTokens.PROTECTED_KEYWORD) ||
ktDeclaration.hasModifier(KtTokens.INTERNAL_KEYWORD))
) {
return
}
val prevComment = node.prevLeaf { it.elementType == ElementType.EOL_COMMENT }
if (prevComment == null || !prevComment.text.startsWith("// [ENTITY:")) {
emit(node.startOffset, "Missing or misplaced '// [ENTITY: ...]' declaration before '${node.elementType}'.", false)
}
}
}
}

View File

@@ -1,4 +1,24 @@
// Путь: data/semantic-ktlint-rules/src/main/java/com/busya/ktlint/rules/NoStrayCommentsRule.kt
package com.busya.ktlint.rules
class NoStrayCommentsRule {
import com.pinterest.ktlint.rule.engine.core.api.ElementType
import com.pinterest.ktlint.rule.engine.core.api.Rule
import com.pinterest.ktlint.rule.engine.core.api.Rule.About
import com.pinterest.ktlint.rule.engine.core.api.RuleId
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
class NoStrayCommentsRule : Rule(ruleId = RuleId("custom:no-stray-comments-rule"), about = About()) {
private val allowedCommentPattern = Regex("""^//\s?\[([A-Z_]+|ENTITY:|RELATION:|AI_NOTE:)]""")
override fun beforeVisitChildNodes(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
) {
if (node.elementType == ElementType.EOL_COMMENT) {
val commentText = node.text
if (!allowedCommentPattern.matches(commentText)) {
emit(node.startOffset, "Stray comment found. Use semantic anchors like '// [TAG]' or '// [AI_NOTE]:' instead.", false)
}
}
}
}

View File

@@ -0,0 +1 @@
com.busya.ktlint.rules.CustomRuleSetProvider

View File

@@ -1,17 +1,41 @@
package com.busya.ktlint.rules
import org.junit.Test
import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule
import org.junit.jupiter.api.Test
import org.junit.Assert.*
class FileHeaderRuleTest {
private val ruleAssertThat = assertThatRule { FileHeaderRule() }
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
fun `should pass on correct header`() {
val code = """
// [PACKAGE] com.example
// [FILE] Test.kt
// [SEMANTICS] test, example
package com.example
""".trimIndent()
ruleAssertThat(code).hasNoLintViolations()
}
@Test
fun `should fail on missing header`() {
val code = """
package com.example
""".trimIndent()
ruleAssertThat(code)
.hasLintViolation(1, 1, "File must start with a 3-line semantic header.")
}
@Test
fun `should fail on incorrect line 1`() {
val code = """
// [WRONG_TAG] com.example
// [FILE] Test.kt
// [SEMANTICS] test, example
package com.example
""".trimIndent()
ruleAssertThat(code)
.hasLintViolation(1, 1, "File header missing or incorrect. Line 1 must be '// [PACKAGE] ...'.")
}
}