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 {
|
tasks.withType<KotlinCompile>().configureEach {
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
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"
|
id = "com.github.android-password-store.kotlin-library"
|
||||||
implementationClass = "app.passwordstore.gradle.KotlinLibraryPlugin"
|
implementationClass = "app.passwordstore.gradle.KotlinLibraryPlugin"
|
||||||
}
|
}
|
||||||
|
register("ktfmt") {
|
||||||
|
id = "com.github.android-password-store.ktfmt"
|
||||||
|
implementationClass = "app.passwordstore.gradle.KtfmtPlugin"
|
||||||
|
}
|
||||||
register("spotless") {
|
register("spotless") {
|
||||||
id = "com.github.android-password-store.spotless"
|
id = "com.github.android-password-store.spotless"
|
||||||
implementationClass = "app.passwordstore.gradle.SpotlessPlugin"
|
implementationClass = "app.passwordstore.gradle.SpotlessPlugin"
|
||||||
|
@ -57,9 +61,12 @@ gradlePlugin {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.build.agp)
|
implementation(libs.build.agp)
|
||||||
implementation(libs.build.detekt)
|
implementation(libs.build.detekt)
|
||||||
|
implementation(libs.build.diffutils)
|
||||||
implementation(libs.build.kotlin)
|
implementation(libs.build.kotlin)
|
||||||
|
implementation(libs.build.ktfmt)
|
||||||
implementation(libs.build.r8)
|
implementation(libs.build.r8)
|
||||||
implementation(libs.build.spotless)
|
implementation(libs.build.spotless)
|
||||||
implementation(libs.build.vcu)
|
implementation(libs.build.vcu)
|
||||||
implementation(libs.build.versions)
|
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")
|
@file:Suppress("DSL_SCOPE_VIOLATION", "UnstableApiUsage")
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.github.android-password-store.kotlin-common")
|
|
||||||
id("com.github.android-password-store.git-hooks")
|
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.spotless")
|
||||||
id("com.github.android-password-store.versions")
|
id("com.github.android-password-store.versions")
|
||||||
alias(libs.plugins.hilt) apply false
|
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"
|
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-agp = { module = "com.android.tools.build:gradle", version.ref = "agp" }
|
||||||
build-detekt = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.21.0"
|
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-download = "de.undercouch:gradle-download-task:5.3.0"
|
||||||
build-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
|
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-mavenpublish = "com.vanniktech:gradle-maven-publish-plugin:0.22.0"
|
||||||
build-metalava = "me.tylerbwong.gradle.metalava:plugin:0.3.2"
|
build-metalava = "me.tylerbwong.gradle.metalava:plugin:0.3.2"
|
||||||
build-okhttp = "com.squareup.okhttp3:okhttp:4.10.0"
|
build-okhttp = "com.squareup.okhttp3:okhttp:4.10.0"
|
||||||
|
|
Loading…
Reference in a new issue