Skip to content

Commit

Permalink
Add script to create all files necessary for a new rule (#352)
Browse files Browse the repository at this point in the history
  • Loading branch information
mrmans0n authored Oct 7, 2024
1 parent f13f3fd commit ea45ecf
Show file tree
Hide file tree
Showing 6 changed files with 256 additions and 0 deletions.
132 changes: 132 additions & 0 deletions scripts/create-rule.main.kts
Original file line number Diff line number Diff line change
@@ -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)
21 changes: 21 additions & 0 deletions scripts/templates/DetektRule.kt.template
Original file line number Diff line number Diff line change
@@ -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,
)
}
42 changes: 42 additions & 0 deletions scripts/templates/DetektRuleTest.kt.template
Original file line number Diff line number Diff line change
@@ -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()
}
}
9 changes: 9 additions & 0 deletions scripts/templates/KtlintRule.kt.template
Original file line number Diff line number Diff line change
@@ -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}()
38 changes: 38 additions & 0 deletions scripts/templates/KtlintRuleTest.kt.template
Original file line number Diff line number Diff line change
@@ -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()
}
}
14 changes: 14 additions & 0 deletions scripts/templates/Rule.kt.template
Original file line number Diff line number Diff line change
@@ -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()
}
}

0 comments on commit ea45ecf

Please sign in to comment.