diff --git a/scripts/create-rule.main.kts b/scripts/create-rule.main.kts new file mode 100755 index 00000000..0b4e522c --- /dev/null +++ b/scripts/create-rule.main.kts @@ -0,0 +1,132 @@ +#!/usr/bin/env kotlin + +@file:DependsOn("org.apache.velocity:velocity-engine-core:2.4") + +import org.apache.velocity.VelocityContext +import org.apache.velocity.app.VelocityEngine +import org.apache.velocity.runtime.RuntimeConstants +import java.io.File +import java.io.StringWriter +import java.util.* +import kotlin.system.exitProcess + +fun printUsage() { + println("Usage: create-rule [RuleName]") + println() +} + +private val humps by lazy { "(?<=.)(?=\\p{Upper})".toRegex() } + +fun String.toKebabCase() = replace(humps, "-").lowercase(Locale.getDefault()) + +fun VelocityEngine.writeTemplate( + templateName: String, + targetDirectory: File, + targetName: String, + context: VelocityContext +) { + val targetFile = targetDirectory.resolve("${targetName}.kt") + if (targetFile.exists()) { + println("Can't write $templateName to $targetFile, file already exists. Delete and run again.") + return + } + println("--> Writing to $targetFile...") + val template = getTemplate(templateName) + val writer = StringWriter() + template.merge(context, writer) + targetFile.writeText(writer.toString()) +} + +// main code + +if (args.isEmpty()) { + printUsage() + exitProcess(1) +} +val newRule = args.singleOrNull() +when { + newRule == null -> { + println("Only 1 parameter supported.") + printUsage() + exitProcess(2) + } + + newRule.endsWith("Rule") || newRule.endsWith("Check") -> { + println("Do not add 'Rule' or 'Check' suffix, it will result in weird and repetitive naming.") + printUsage() + exitProcess(2) + } +} + +val ruleName = requireNotNull(newRule) + +println("Finding project root...") +var rootDir = File(System.getProperty("user.dir")) +while (!rootDir.resolve("settings.gradle.kts").exists()) { + rootDir = rootDir.parentFile +} + +println("Setting up templates...") + +val engine = VelocityEngine( + Properties().apply { + setProperty( + RuntimeConstants.FILE_RESOURCE_LOADER_PATH, + "templates" + ) // Adjust the path to your templates directory + } +).apply { init() } + +val context = VelocityContext() +context.apply { + put("ruleName", ruleName) + put("detektRuleName", "${newRule}Check") + put("ktlintRuleName", "${newRule}Check") + put("ktlintRuleId", ruleName.toKebabCase()) +} + +println("Applying templates...") + +// Write main rule +engine.writeTemplate( + templateName = "Rule.kt.template", + targetDirectory = rootDir.resolve("rules/common/src/main/kotlin/io/nlopez/compose/rules/"), + targetName = ruleName, + context = context +) + +// Write detekt rule that delegates to main rule +engine.writeTemplate( + templateName = "DetektRule.kt.template", + targetDirectory = rootDir.resolve("rules/detekt/src/main/kotlin/io/nlopez/compose/rules/detekt/"), + targetName = "${ruleName}Check", + context = context +) + +// Write test for detekt rule +engine.writeTemplate( + templateName = "DetektRuleTest.kt.template", + targetDirectory = rootDir.resolve("rules/detekt/src/test/kotlin/io/nlopez/compose/rules/detekt/"), + targetName = "${ruleName}CheckTest", + context = context +) + +// Write ktlint rule that delegates to main rule +engine.writeTemplate( + templateName = "KtlintRule.kt.template", + targetDirectory = rootDir.resolve("rules/ktlint/src/main/kotlin/io/nlopez/compose/rules/ktlint/"), + targetName = "${ruleName}Check", + context = context +) + +// Write test for ktlint rule +engine.writeTemplate( + templateName = "KtlintRuleTest.kt.template", + targetDirectory = rootDir.resolve("rules/ktlint/src/test/kotlin/io/nlopez/compose/rules/ktlint/"), + targetName = "${ruleName}CheckTest", + context = context +) +// Desirable improvements to add: +// - add to detekt's default ruleset yml rules/detekt/src/main/resources/config/config.yml +// - add rule to docs/detekt.md "default rule" values +// - add entry in docs/rules.md (likely at the end) diff --git a/scripts/templates/DetektRule.kt.template b/scripts/templates/DetektRule.kt.template new file mode 100644 index 00000000..a9b9f271 --- /dev/null +++ b/scripts/templates/DetektRule.kt.template @@ -0,0 +1,21 @@ +package io.nlopez.compose.rules.detekt + +import io.gitlab.arturbosch.detekt.api.Config +import io.gitlab.arturbosch.detekt.api.Debt +import io.gitlab.arturbosch.detekt.api.Issue +import io.gitlab.arturbosch.detekt.api.Severity +import io.nlopez.compose.core.ComposeKtVisitor +import io.nlopez.compose.rules.${ruleName} +import io.nlopez.compose.rules.DetektRule + +class ${detektRuleName}(config: Config) : + DetektRule(config), + ComposeKtVisitor by ${ruleName}() { + + override val issue: Issue = Issue( + id = "${ruleName}", + severity = Severity.CodeSmell, + description = ${ruleName}.${ruleName}ErrorMessage, + debt = Debt.FIVE_MINS, + ) +} diff --git a/scripts/templates/DetektRuleTest.kt.template b/scripts/templates/DetektRuleTest.kt.template new file mode 100644 index 00000000..ec03407d --- /dev/null +++ b/scripts/templates/DetektRuleTest.kt.template @@ -0,0 +1,42 @@ +package io.nlopez.compose.rules.detekt + +import io.gitlab.arturbosch.detekt.api.Config +import io.gitlab.arturbosch.detekt.api.SourceLocation +import io.gitlab.arturbosch.detekt.test.assertThat +import io.gitlab.arturbosch.detekt.test.lint +import io.nlopez.compose.rules.${ruleName} +import org.intellij.lang.annotations.Language +import org.junit.jupiter.api.Test + +class ${detektRuleName}Test { + + private val rule = ${detektRuleName}(Config.empty) + + @Test + fun `errors for X case`() { + @Language("kotlin") + val code = + """ + TODO() + """.trimIndent() + val errors = rule.lint(code) + assertThat(errors).hasStartSourceLocations( + SourceLocation(2, 5), + ) + for (error in errors) { + assertThat(error) + .hasMessage(${ruleName}.${ruleName}ErrorMessage) + } + } + + @Test + fun `passes for X case`() { + @Language("kotlin") + val code = + """ + TODO() + """.trimIndent() + val errors = rule.lint(code) + assertThat(errors).isEmpty() + } +} diff --git a/scripts/templates/KtlintRule.kt.template b/scripts/templates/KtlintRule.kt.template new file mode 100644 index 00000000..aba942ac --- /dev/null +++ b/scripts/templates/KtlintRule.kt.template @@ -0,0 +1,9 @@ +package io.nlopez.compose.rules.ktlint + +import io.nlopez.compose.core.ComposeKtVisitor +import io.nlopez.compose.rules.${ruleName} +import io.nlopez.compose.rules.KtlintRule + +class ${ruleName} : + KtlintRule("compose:${ktlintRuleId}"), + ComposeKtVisitor by ${ruleName}() diff --git a/scripts/templates/KtlintRuleTest.kt.template b/scripts/templates/KtlintRuleTest.kt.template new file mode 100644 index 00000000..3a8edc95 --- /dev/null +++ b/scripts/templates/KtlintRuleTest.kt.template @@ -0,0 +1,38 @@ +package io.nlopez.compose.rules.ktlint + +import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule +import com.pinterest.ktlint.test.LintViolation +import io.nlopez.compose.rules.${ruleName} +import org.intellij.lang.annotations.Language +import org.junit.jupiter.api.Test + +class ${ktlintRuleName}Test { + + private val ruleAssertThat = assertThatRule { ${ktlintRuleName}() } + + @Test + fun `errors for X case`() { + @Language("kotlin") + val code = + """ + TODO() + """.trimIndent() + ruleAssertThat(code).hasLintViolationsWithoutAutoCorrect( + LintViolation( + line = 2, + col = 5, + detail = ${ruleName}.${ruleName}ErrorMessage, + ), + ) + } + + @Test + fun `passes for X case`() { + @Language("kotlin") + val code = + """ + TODO() + """.trimIndent() + ruleAssertThat(code).hasNoLintViolations() + } +} diff --git a/scripts/templates/Rule.kt.template b/scripts/templates/Rule.kt.template new file mode 100644 index 00000000..a6d9b083 --- /dev/null +++ b/scripts/templates/Rule.kt.template @@ -0,0 +1,14 @@ +package io.nlopez.compose.rules + +import io.nlopez.compose.core.ComposeKtVisitor + +class ${ruleName} : ComposeKtVisitor { + + companion object { + val ${ruleName}ErrorMessage = """ + TODO + + See https://mrmans0n.github.io/compose-rules/rules/#TODO for more information. + """.trimIndent() + } +}