diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 71995b37..ecbc5a60 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -68,6 +68,16 @@ "datasourceTemplate": "maven", "depNameTemplate": "androidx.compose.compiler:compiler", "registryUrlTemplate": "https://maven.google.com", - } + }, + { + "fileMatch": [ + "build-logic/src/main/kotlin/app/passwordstore/gradle/SpotlessPlugin.kt" + ], + "matchStrings": [ + "KTFMT_VERSION = \"(?.*)\"" + ], + "datasourceTemplate": "maven", + "depNameTemplate": "com.facebook:ktfmt" + } ] } diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index f5879ab7..242065f6 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -20,7 +20,7 @@ jobs: - name: Check codestyle shell: bash - run: ./gradlew ktfmtCheck + run: ./gradlew spotlessCheck - name: Upload Kotlin build report if: "${{ always() }}" diff --git a/app/src/main/res/values-pl-rPL/strings.xml b/app/src/main/res/values-pl-rPL/strings.xml index 0b3bee8f..e3d57b79 100644 --- a/app/src/main/res/values-pl-rPL/strings.xml +++ b/app/src/main/res/values-pl-rPL/strings.xml @@ -6,7 +6,7 @@ Wybrano %d element - Wybrano %d elementy + Wybrano %d elementy (If: 2,3,4) Wybrano %d elementów Liczba wybranych elementów: %d diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index 4917fc8c..1f98dde8 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -41,10 +41,6 @@ gradlePlugin { id = "com.github.android-password-store.kotlin-jvm-library" implementationClass = "app.passwordstore.gradle.KotlinJVMLibrary" } - register("ktfmt") { - id = "com.github.android-password-store.ktfmt" - implementationClass = "app.passwordstore.gradle.KtfmtPlugin" - } register("published-android-library") { id = "com.github.android-password-store.published-android-library" implementationClass = "app.passwordstore.gradle.PublishedAndroidLibraryPlugin" @@ -61,6 +57,10 @@ gradlePlugin { id = "com.github.android-password-store.sentry" implementationClass = "app.passwordstore.gradle.SentryPlugin" } + register("spotless") { + id = "com.github.android-password-store.spotless" + implementationClass = "app.passwordstore.gradle.SpotlessPlugin" + } register("versioning") { id = "com.github.android-password-store.versioning-plugin" implementationClass = "app.passwordstore.gradle.versioning.VersioningPlugin" @@ -79,7 +79,6 @@ dependencies { implementation(libs.build.download) implementation(libs.build.javapoet) implementation(libs.build.kotlin) - implementation(libs.build.ktfmt) implementation(libs.build.mavenpublish) implementation(libs.build.metalava) implementation(libs.build.moshi) @@ -88,6 +87,7 @@ dependencies { implementation(libs.build.r8) implementation(libs.build.semver) implementation(libs.build.sentry) + implementation(libs.build.spotless) implementation(libs.build.vcu) implementation(libs.kotlinx.coroutines.core) diff --git a/build-logic/src/main/kotlin/app/passwordstore/gradle/KtfmtPlugin.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/KtfmtPlugin.kt deleted file mode 100644 index 7aac48b5..00000000 --- a/build-logic/src/main/kotlin/app/passwordstore/gradle/KtfmtPlugin.kt +++ /dev/null @@ -1,37 +0,0 @@ -package app.passwordstore.gradle - -import app.passwordstore.gradle.ktfmt.KtfmtCheckTask -import app.passwordstore.gradle.ktfmt.KtfmtFormatTask -import com.facebook.ktfmt.format.FormattingOptions -import java.util.concurrent.Callable -import org.gradle.api.Plugin -import org.gradle.api.Project -import org.gradle.kotlin.dsl.register - -class KtfmtPlugin : Plugin { - - override fun apply(target: Project) { - val input = Callable { - target.layout.projectDirectory.asFileTree.filter { file -> - file.extension == "kt" || file.extension == "kts" && !file.canonicalPath.contains("build/") - } - } - target.tasks.register("ktfmtFormat") { source(input) } - target.tasks.register("ktfmtCheck") { - source(input) - projectDirectory.set(target.layout.projectDirectory) - } - } - - companion object { - val DEFAULT_FORMATTING_OPTIONS = - FormattingOptions( - maxWidth = FormattingOptions.DEFAULT_MAX_WIDTH, - blockIndent = 2, - continuationIndent = 2, - removeUnusedImports = true, - debuggingPrintOpsAfterFormatting = false, - manageTrailingCommas = true, - ) - } -} diff --git a/build-logic/src/main/kotlin/app/passwordstore/gradle/SpotlessPlugin.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/SpotlessPlugin.kt new file mode 100644 index 00000000..6f33f922 --- /dev/null +++ b/build-logic/src/main/kotlin/app/passwordstore/gradle/SpotlessPlugin.kt @@ -0,0 +1,44 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.gradle + +import com.diffplug.gradle.spotless.SpotlessExtension +import com.diffplug.gradle.spotless.SpotlessPlugin +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.apply +import org.gradle.kotlin.dsl.getByType + +@Suppress("Unused") +class SpotlessPlugin : Plugin { + + override fun apply(project: Project) { + project.pluginManager.apply(SpotlessPlugin::class) + project.extensions.getByType().run { + kotlin { + ktfmt(KTFMT_VERSION).googleStyle() + target("**/*.kt") + targetExclude("**/build/") + } + kotlinGradle { + ktfmt(KTFMT_VERSION).googleStyle() + target("**/*.kts") + targetExclude("**/build/") + } + format("xml") { + target("**/*.xml") + targetExclude("**/build/", ".idea/") + trimTrailingWhitespace() + indentWithSpaces() + endWithNewline() + } + } + } + + private companion object { + private const val KTFMT_VERSION = "0.51" + } +} diff --git a/build-logic/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtCheckTask.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtCheckTask.kt deleted file mode 100644 index 58ef432f..00000000 --- a/build-logic/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtCheckTask.kt +++ /dev/null @@ -1,63 +0,0 @@ -package app.passwordstore.gradle.ktfmt - -import app.passwordstore.gradle.KtfmtPlugin -import com.facebook.ktfmt.format.Formatter -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.GradleException -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 }) { - val prettyDiff = - results - .map { (_, diffs) -> diffs } - .flatten() - .joinToString(separator = "\n") { diff -> diff.toString() } - throw GradleException("[ktfmt] Found unformatted files\n${prettyDiff}") - } - } - } - } - - private fun checkFile(input: File): Pair> { - val originCode = input.readText() - val formattedCode = Formatter.format(KtfmtPlugin.DEFAULT_FORMATTING_OPTIONS, 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/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtDiffEntry.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtDiffEntry.kt deleted file mode 100644 index ca01b5e9..00000000 --- a/build-logic/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtDiffEntry.kt +++ /dev/null @@ -1,8 +0,0 @@ -package app.passwordstore.gradle.ktfmt - -data class KtfmtDiffEntry(val input: String, val lineNumber: Int, val message: String) { - - override fun toString(): String { - return "$input:$lineNumber - $message" - } -} diff --git a/build-logic/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtDiffer.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtDiffer.kt deleted file mode 100644 index f1332923..00000000 --- a/build-logic/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtDiffer.kt +++ /dev/null @@ -1,28 +0,0 @@ -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 - -object KtfmtDiffer { - fun computeDiff( - inputFile: File, - formattedCode: String, - pathNormalizer: (File) -> String, - ): List { - 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) - } - } -} diff --git a/build-logic/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtFormatTask.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtFormatTask.kt deleted file mode 100644 index c76b5c89..00000000 --- a/build-logic/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtFormatTask.kt +++ /dev/null @@ -1,46 +0,0 @@ -package app.passwordstore.gradle.ktfmt - -import javax.inject.Inject -import org.gradle.api.GradleException -import org.gradle.api.file.ProjectLayout -import org.gradle.api.tasks.SourceTask -import org.gradle.api.tasks.TaskAction -import org.gradle.internal.exceptions.MultiCauseException -import org.gradle.workers.WorkerExecutor -import org.jetbrains.kotlin.utils.addToStdlib.ifNotEmpty - -abstract class KtfmtFormatTask -@Inject -constructor(private val workerExecutor: WorkerExecutor, private val projectLayout: ProjectLayout) : - SourceTask() { - - @TaskAction - fun execute() { - val result = - with(workerExecutor.noIsolation()) { - submit(KtfmtWorkerAction::class.java) { - name.set("ktfmt-worker") - files.from(source) - projectDirectory.set(projectLayout.projectDirectory.asFile) - } - runCatching { await() } - } - - result.exceptionOrNull()?.workErrorCauses()?.ifNotEmpty { - forEach { logger.error(it.message, it.cause) } - throw GradleException("error formatting sources for $name") - } - } - - private inline fun Throwable.workErrorCauses(): List { - return when (this) { - is MultiCauseException -> this.causes.map { it.cause } - else -> listOf(this.cause) - } - .filter { - // class instance comparison doesn't work due to different classloaders - it?.javaClass?.canonicalName == T::class.java.canonicalName - } - .filterNotNull() - } -} diff --git a/build-logic/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtWorkerAction.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtWorkerAction.kt deleted file mode 100644 index c955adbe..00000000 --- a/build-logic/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtWorkerAction.kt +++ /dev/null @@ -1,38 +0,0 @@ -package app.passwordstore.gradle.ktfmt - -import app.passwordstore.gradle.KtfmtPlugin -import com.facebook.ktfmt.format.Formatter -import java.io.File -import org.gradle.api.logging.LogLevel -import org.gradle.api.logging.Logger -import org.gradle.api.logging.Logging -import org.gradle.internal.logging.slf4j.DefaultContextAwareTaskLogger -import org.gradle.workers.WorkAction - -abstract class KtfmtWorkerAction : WorkAction { - private val logger: Logger = - DefaultContextAwareTaskLogger(Logging.getLogger(KtfmtFormatTask::class.java)) - private val files: List = parameters.files.toList() - private val projectDirectory: File = parameters.projectDirectory.asFile.get() - private val name: String = parameters.name.get() - - override fun execute() { - try { - files.forEach { file -> - val sourceText = file.readText() - val relativePath = file.toRelativeString(projectDirectory) - - logger.log(LogLevel.DEBUG, "$name checking format: $relativePath") - - val formattedText = Formatter.format(KtfmtPlugin.DEFAULT_FORMATTING_OPTIONS, sourceText) - - if (!formattedText.contentEquals(sourceText)) { - logger.log(LogLevel.QUIET, "${file.toRelativeString(projectDirectory)}: Format fixed") - file.writeText(formattedText) - } - } - } catch (t: Throwable) { - throw Exception("format worker execution error", t) - } - } -} diff --git a/build-logic/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtWorkerParameters.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtWorkerParameters.kt deleted file mode 100644 index 1c550de8..00000000 --- a/build-logic/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtWorkerParameters.kt +++ /dev/null @@ -1,12 +0,0 @@ -package app.passwordstore.gradle.ktfmt - -import org.gradle.api.file.ConfigurableFileCollection -import org.gradle.api.file.RegularFileProperty -import org.gradle.api.provider.Property -import org.gradle.workers.WorkParameters - -interface KtfmtWorkerParameters : WorkParameters { - val name: Property - val files: ConfigurableFileCollection - val projectDirectory: RegularFileProperty -} diff --git a/build.gradle.kts b/build.gradle.kts index aac6cf2b..8e6bedb4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,6 +5,6 @@ plugins { 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") } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 947c2983..45f63a68 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -39,7 +39,6 @@ build-diffutils = "io.github.java-diff-utils:java-diff-utils:4.12" build-download = "de.undercouch:gradle-download-task:5.6.0" build-javapoet = "com.squareup:javapoet:1.13.0" build-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } -build-ktfmt = "com.facebook:ktfmt:0.51" build-mavenpublish = "com.vanniktech:gradle-maven-publish-plugin:0.29.0" build-metalava = "me.tylerbwong.gradle.metalava:plugin:0.3.5" build-moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" } @@ -48,6 +47,7 @@ build-okhttp = "com.squareup.okhttp3:okhttp:5.0.0-alpha.14" build-r8 = "com.android.tools:r8:8.4.6-dev" build-semver = "com.github.zafarkhaja:java-semver:0.10.2" build-sentry = "io.sentry.android.gradle:io.sentry.android.gradle.gradle.plugin:4.11.0" +build-spotless = "com.diffplug.spotless:spotless-plugin-gradle:6.25.0" build-vcu = "nl.littlerobots.version-catalog-update:nl.littlerobots.version-catalog-update.gradle.plugin:0.8.4" compose-bom = "androidx.compose:compose-bom:2024.06.00" compose-foundation-core = { module = "androidx.compose.foundation:foundation" } diff --git a/scripts/pre-push-hook.sh b/scripts/pre-push-hook.sh index 65ea0682..e228815c 100644 --- a/scripts/pre-push-hook.sh +++ b/scripts/pre-push-hook.sh @@ -13,6 +13,6 @@ while read -r local_ref local_oid remote_ref remote_oid; do _=$remote_ref _=$remote_oid if [ "${local_oid}" != "${ZERO}" ]; then - CI=true "${GRADLE_EXEC}" metalavaCheckCompatibilityRelease lint ktfmtCheck test -PslimTests + CI=true "${GRADLE_EXEC}" metalavaCheckCompatibilityRelease lint spotlessCheck test -PslimTests fi done