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:
parent
505c2fa705
commit
fd20480f55
8 changed files with 206 additions and 2 deletions
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
package app.passwordstore.gradle.ktfmt
|
||||
|
||||
data class KtfmtDiffEntry(val input: String, val lineNumber: Int, val message: String)
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue