aboutsummaryrefslogtreecommitdiff
path: root/build-logic/kotlin-plugins
diff options
context:
space:
mode:
authorHarsh Shandilya <me@msfjarvis.dev>2022-10-29 07:29:46 +0530
committerHarsh Shandilya <me@msfjarvis.dev>2022-10-29 08:00:28 +0530
commitfd20480f554060805acba3124cb251be7824c4d2 (patch)
tree5632081000dd955d51c04397733b0faeabd16b04 /build-logic/kotlin-plugins
parent505c2fa705fca1aaec7aae80c50a939bfd607716 (diff)
feat(build): add a homebrew ktfmt plugin
The general idea of the implementation is borrowed from https://github.com/cortinico/ktfmt-gradle
Diffstat (limited to 'build-logic/kotlin-plugins')
-rw-r--r--build-logic/kotlin-plugins/build.gradle.kts9
-rw-r--r--build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/KtfmtPlugin.kt32
-rw-r--r--build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtCheckTask.kt68
-rw-r--r--build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtDiffEntry.kt3
-rw-r--r--build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtDiffer.kt35
-rw-r--r--build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtFormatTask.kt56
6 files changed, 202 insertions, 1 deletions
diff --git a/build-logic/kotlin-plugins/build.gradle.kts b/build-logic/kotlin-plugins/build.gradle.kts
index fc08cd15..815d6ca0 100644
--- a/build-logic/kotlin-plugins/build.gradle.kts
+++ b/build-logic/kotlin-plugins/build.gradle.kts
@@ -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)
}
diff --git a/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/KtfmtPlugin.kt b/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/KtfmtPlugin.kt
new file mode 100644
index 00000000..74e8ba95
--- /dev/null
+++ b/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/KtfmtPlugin.kt
@@ -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)
+ }
+ }
+}
diff --git a/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtCheckTask.kt b/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtCheckTask.kt
new file mode 100644
index 00000000..a50c8494
--- /dev/null
+++ b/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtCheckTask.kt
@@ -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
+ }
+}
diff --git a/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtDiffEntry.kt b/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtDiffEntry.kt
new file mode 100644
index 00000000..44d1a967
--- /dev/null
+++ b/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtDiffEntry.kt
@@ -0,0 +1,3 @@
+package app.passwordstore.gradle.ktfmt
+
+data class KtfmtDiffEntry(val input: String, val lineNumber: Int, val message: String)
diff --git a/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtDiffer.kt b/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtDiffer.kt
new file mode 100644
index 00000000..936596cd
--- /dev/null
+++ b/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtDiffer.kt
@@ -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}")
+ }
+ }
+}
diff --git a/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtFormatTask.kt b/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtFormatTask.kt
new file mode 100644
index 00000000..82ce4ca3
--- /dev/null
+++ b/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtFormatTask.kt
@@ -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
+ }
+}