feat(build): add a homebrew ktfmt plugin

The general idea of the implementation is borrowed from https://github.com/cortinico/ktfmt-gradle
This commit is contained in:
Harsh Shandilya 2022-10-29 07:29:46 +05:30
parent 505c2fa705
commit fd20480f55
No known key found for this signature in database
8 changed files with 206 additions and 2 deletions

View file

@ -20,7 +20,7 @@ afterEvaluate {
tasks.withType<KotlinCompile>().configureEach {
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
freeCompilerArgs = freeCompilerArgs + "-Xsam-conversions=class"
freeCompilerArgs = freeCompilerArgs + "-Xsam-conversions=class" + "-opt-in=kotlin.RequiresOptIn"
}
}
}
@ -43,6 +43,10 @@ gradlePlugin {
id = "com.github.android-password-store.kotlin-library"
implementationClass = "app.passwordstore.gradle.KotlinLibraryPlugin"
}
register("ktfmt") {
id = "com.github.android-password-store.ktfmt"
implementationClass = "app.passwordstore.gradle.KtfmtPlugin"
}
register("spotless") {
id = "com.github.android-password-store.spotless"
implementationClass = "app.passwordstore.gradle.SpotlessPlugin"
@ -57,9 +61,12 @@ gradlePlugin {
dependencies {
implementation(libs.build.agp)
implementation(libs.build.detekt)
implementation(libs.build.diffutils)
implementation(libs.build.kotlin)
implementation(libs.build.ktfmt)
implementation(libs.build.r8)
implementation(libs.build.spotless)
implementation(libs.build.vcu)
implementation(libs.build.versions)
implementation(libs.kotlin.coroutines.core)
}

View file

@ -0,0 +1,32 @@
package app.passwordstore.gradle
import app.passwordstore.gradle.ktfmt.KtfmtCheckTask
import app.passwordstore.gradle.ktfmt.KtfmtFormatTask
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.register
class KtfmtPlugin : Plugin<Project> {
override fun apply(target: Project) {
target.tasks.register<KtfmtFormatTask>("ktfmtFormat") {
source =
project.layout.projectDirectory.asFileTree
.filter { file ->
file.extension == "kt" ||
file.extension == "kts" && !file.canonicalPath.contains("build")
}
.asFileTree
}
target.tasks.register<KtfmtCheckTask>("ktfmtCheck") {
source =
project.layout.projectDirectory.asFileTree
.filter { file ->
file.extension == "kt" ||
file.extension == "kts" && !file.canonicalPath.contains("build")
}
.asFileTree
projectDirectory.set(target.layout.projectDirectory)
}
}
}

View file

@ -0,0 +1,68 @@
package app.passwordstore.gradle.ktfmt
import com.facebook.ktfmt.format.Formatter
import com.facebook.ktfmt.format.FormattingOptions
import java.io.File
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.runBlocking
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileCollection
import org.gradle.api.tasks.IgnoreEmptyDirectories
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.SourceTask
import org.gradle.api.tasks.TaskAction
@OptIn(ExperimentalCoroutinesApi::class)
abstract class KtfmtCheckTask : SourceTask() {
@get:PathSensitive(PathSensitivity.RELATIVE)
@get:InputFiles
@get:IgnoreEmptyDirectories
protected val inputFiles: FileCollection
get() = super.getSource()
@get:Internal abstract val projectDirectory: DirectoryProperty
@TaskAction
fun execute() {
runBlocking(Dispatchers.IO.limitedParallelism(PARALLEL_TASK_LIMIT)) {
coroutineScope {
val results = inputFiles.map { async { checkFile(it) } }.awaitAll()
if (results.any { (notFormatted, _) -> notFormatted }) {
results
.map { (_, diffs) -> diffs }
.forEach { diffs -> KtfmtDiffer.printDiff(diffs, logger) }
error("[ktfmt] Found unformatted files")
}
}
}
}
private fun checkFile(input: File): Pair<Boolean, List<KtfmtDiffEntry>> {
val originCode = input.readText()
val formattedCode =
Formatter.format(
FormattingOptions(
style = FormattingOptions.Style.GOOGLE,
maxWidth = 100,
continuationIndent = 2,
),
originCode
)
val pathNormalizer = { file: File -> file.toRelativeString(projectDirectory.asFile.get()) }
return (originCode != formattedCode) to
KtfmtDiffer.computeDiff(input, formattedCode, pathNormalizer)
}
companion object {
private const val PARALLEL_TASK_LIMIT = 4
}
}

View file

@ -0,0 +1,3 @@
package app.passwordstore.gradle.ktfmt
data class KtfmtDiffEntry(val input: String, val lineNumber: Int, val message: String)

View file

@ -0,0 +1,35 @@
package app.passwordstore.gradle.ktfmt
import com.github.difflib.DiffUtils
import com.github.difflib.patch.ChangeDelta
import com.github.difflib.patch.DeleteDelta
import com.github.difflib.patch.InsertDelta
import java.io.File
import org.gradle.api.logging.Logger
object KtfmtDiffer {
fun computeDiff(
inputFile: File,
formattedCode: String,
pathNormalizer: (File) -> String
): List<KtfmtDiffEntry> {
val originCode = inputFile.readText()
return DiffUtils.diff(originCode, formattedCode, null).deltas.map {
val line = it.source.position + 1
val message: String =
when (it) {
is ChangeDelta -> "Line changed: ${it.source.lines.first()}"
is DeleteDelta -> "Line deleted"
is InsertDelta -> "Line added"
else -> ""
}
KtfmtDiffEntry(pathNormalizer(inputFile), line, message)
}
}
fun printDiff(entries: List<KtfmtDiffEntry>, logger: Logger) {
entries.forEach { entry ->
logger.error("${entry.input}:${entry.lineNumber} - ${entry.message}")
}
}
}

View file

@ -0,0 +1,56 @@
package app.passwordstore.gradle.ktfmt
import com.facebook.ktfmt.format.Formatter
import com.facebook.ktfmt.format.FormattingOptions
import java.io.File
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.runBlocking
import org.gradle.api.file.FileCollection
import org.gradle.api.tasks.IgnoreEmptyDirectories
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.SourceTask
import org.gradle.api.tasks.TaskAction
@OptIn(ExperimentalCoroutinesApi::class)
abstract class KtfmtFormatTask : SourceTask() {
@get:PathSensitive(PathSensitivity.RELATIVE)
@get:InputFiles
@get:IgnoreEmptyDirectories
protected val inputFiles: FileCollection
get() = super.getSource()
@TaskAction
fun execute() {
runBlocking(Dispatchers.IO.limitedParallelism(PARALLEL_TASK_LIMIT)) {
coroutineScope { inputFiles.map { async { formatFile(it) } }.awaitAll() }
}
}
private fun formatFile(input: File) {
val originCode = input.readText()
val formattedCode =
Formatter.format(
FormattingOptions(
style = FormattingOptions.Style.GOOGLE,
maxWidth = 100,
continuationIndent = 2,
),
originCode
)
if (originCode != formattedCode) {
input.writeText(formattedCode)
}
}
companion object {
private const val PARALLEL_TASK_LIMIT = 4
}
}

View file

@ -5,8 +5,9 @@
@file:Suppress("DSL_SCOPE_VIOLATION", "UnstableApiUsage")
plugins {
id("com.github.android-password-store.kotlin-common")
id("com.github.android-password-store.git-hooks")
id("com.github.android-password-store.kotlin-common")
id("com.github.android-password-store.ktfmt")
id("com.github.android-password-store.spotless")
id("com.github.android-password-store.versions")
alias(libs.plugins.hilt) apply false

View file

@ -39,8 +39,10 @@ aps-sublimeFuzzy = "com.github.android-password-store:sublime-fuzzy:2.2.1"
aps-zxingAndroidEmbedded = "com.github.android-password-store:zxing-android-embedded:4.2.1"
build-agp = { module = "com.android.tools.build:gradle", version.ref = "agp" }
build-detekt = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.21.0"
build-diffutils = "io.github.java-diff-utils:java-diff-utils:4.12"
build-download = "de.undercouch:gradle-download-task:5.3.0"
build-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
build-ktfmt = "com.facebook:ktfmt:0.41"
build-mavenpublish = "com.vanniktech:gradle-maven-publish-plugin:0.22.0"
build-metalava = "me.tylerbwong.gradle.metalava:plugin:0.3.2"
build-okhttp = "com.squareup.okhttp3:okhttp:4.10.0"