diff --git a/README.md b/README.md index 2e2fd6a..442afbb 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,8 @@ ktlint { - `chunkSize` - defines how many files will be processed by a single gradle worker in parallel - `baselineFile` - points at location of baseline file containing _known_ offenses that will be ignored during `lintKotlin` task execution - `ignoreFilesUnderBuildDir` - This allows to ignore generated sources. This is a workaround for https://youtrack.jetbrains.com/issue/KT-45161. Setting the value to `false` restores default behavior and will run ktlint against all sources returned by KGP +- `editorConfigValidation` - One of `None`, `PrintWarningLogs`, `BuildFailure`. + - Currently, this only validates if any of the _typical_ editorconfig location contain `root=true` entry. This is highly recommended to ensure the builds are deterministic across machines ### Customizing Tasks diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ff0e239..16c800f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,6 +8,7 @@ maven-junit = "5.10.2" maven-assertj = "3.25.3" maven-ktlint = "1.2.1" maven-commons = "2.15.1" +maven-binarycompatibilityvalidator = "0.14.0" [libraries] agp-gradle = { module = "com.android.tools.build:gradle", version.ref = "google-agp" } @@ -21,7 +22,7 @@ junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.re junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "maven-junit" } assertj-core = { module = "org.assertj:assertj-core", version.ref = "maven-assertj" } commons-io = { module = "commons-io:commons-io", version.ref = "maven-commons" } -google-ksp-gradle = { module = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin", version.ref = "google-ksp"} +google-ksp-gradle = { module = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin", version.ref = "google-ksp" } [plugins] starter-config = { id = "com.starter.config", version.ref = "gradle-starter" } @@ -29,3 +30,4 @@ starter-versioning = { id = "com.starter.versioning", version.ref = "gradle-star starter-library-kotlin = { id = "com.starter.library.kotlin", version.ref = "gradle-starter" } gradle-pluginpublish = { id = "com.gradle.plugin-publish", version.ref = "gradle-pluginpublish" } osacky-doctor = { id = "com.osacky.doctor", version.ref = "gradle-doctor" } +kotlinx-binarycompatibilityvalidator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "maven-binarycompatibilityvalidator" } diff --git a/ktlint-gradle-plugin/api/ktlint-gradle-plugin.api b/ktlint-gradle-plugin/api/ktlint-gradle-plugin.api new file mode 100644 index 0000000..4b2b691 --- /dev/null +++ b/ktlint-gradle-plugin/api/ktlint-gradle-plugin.api @@ -0,0 +1,78 @@ +public final class io/github/usefulness/EditorConfigValidationMode : java/lang/Enum { + public static final field BuildFailure Lio/github/usefulness/EditorConfigValidationMode; + public static final field None Lio/github/usefulness/EditorConfigValidationMode; + public static final field PrintWarningLogs Lio/github/usefulness/EditorConfigValidationMode; + public static fun valueOf (Ljava/lang/String;)Lio/github/usefulness/EditorConfigValidationMode; + public static fun values ()[Lio/github/usefulness/EditorConfigValidationMode; +} + +public class io/github/usefulness/KtlintGradleExtension { + public static final field DEFAULT_CHUNK_SIZE I + public static final field DEFAULT_EXPERIMENTAL_RULES Z + public static final field DEFAULT_IGNORE_FAILURES Z + public final fun editorConfigValidation (Ljava/lang/Object;)V + public final fun getBaselineFile ()Lorg/gradle/api/file/RegularFileProperty; + public final fun getChunkSize ()Lorg/gradle/api/provider/Property; + public final fun getDisabledRules ()Lorg/gradle/api/provider/ListProperty; + public final fun getEditorConfigValidation ()Lorg/gradle/api/provider/Property; + public final fun getExperimentalRules ()Lorg/gradle/api/provider/Property; + public final fun getIgnoreFailures ()Lorg/gradle/api/provider/Property; + public final fun getIgnoreFilesUnderBuildDir ()Lorg/gradle/api/provider/Property; + public final fun getIgnoreKspGeneratedSources ()Lorg/gradle/api/provider/Property; + public final fun getKtlintVersion ()Lorg/gradle/api/provider/Property; + public final fun getReporters ()Lorg/gradle/api/provider/ListProperty; +} + +public final class io/github/usefulness/KtlintGradlePlugin : org/gradle/api/Plugin { + public fun ()V + public synthetic fun apply (Ljava/lang/Object;)V + public fun apply (Lorg/gradle/api/Project;)V +} + +public class io/github/usefulness/tasks/CheckEditorConfigTask : org/gradle/api/DefaultTask { + public fun (Lorg/gradle/api/model/ObjectFactory;)V + public final fun getMode ()Lorg/gradle/api/provider/Property; + public final fun run ()V +} + +public class io/github/usefulness/tasks/FormatTask : io/github/usefulness/tasks/KtlintWorkTask { + public fun (Lorg/gradle/workers/WorkerExecutor;Lorg/gradle/api/model/ObjectFactory;Lorg/gradle/api/file/ProjectLayout;)V + public final fun run (Lorg/gradle/work/InputChanges;)V +} + +public abstract class io/github/usefulness/tasks/KtlintWorkTask : org/gradle/api/DefaultTask, org/gradle/api/tasks/util/PatternFilterable { + public fun (Lorg/gradle/workers/WorkerExecutor;Lorg/gradle/api/file/ProjectLayout;Lorg/gradle/api/model/ObjectFactory;Lorg/gradle/api/tasks/util/PatternFilterable;)V + public synthetic fun (Lorg/gradle/workers/WorkerExecutor;Lorg/gradle/api/file/ProjectLayout;Lorg/gradle/api/model/ObjectFactory;Lorg/gradle/api/tasks/util/PatternFilterable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun exclude (Lgroovy/lang/Closure;)Lorg/gradle/api/tasks/util/PatternFilterable; + public fun exclude (Ljava/lang/Iterable;)Lorg/gradle/api/tasks/util/PatternFilterable; + public fun exclude (Lorg/gradle/api/specs/Spec;)Lorg/gradle/api/tasks/util/PatternFilterable; + public fun exclude ([Ljava/lang/String;)Lorg/gradle/api/tasks/util/PatternFilterable; + public final fun getBaselineFile ()Lorg/gradle/api/file/RegularFileProperty; + public final fun getChunkSize ()Lorg/gradle/api/provider/Property; + public final fun getDisabledRules ()Lorg/gradle/api/provider/ListProperty; + public fun getExcludes ()Ljava/util/Set; + public final fun getExperimentalRules ()Lorg/gradle/api/provider/Property; + public final fun getIgnoreFailures ()Lorg/gradle/api/provider/Property; + public fun getIncludes ()Ljava/util/Set; + public final fun getJvmArgs ()Lorg/gradle/api/provider/ListProperty; + public final fun getKtlintClasspath ()Lorg/gradle/api/file/ConfigurableFileCollection; + public final fun getReportersConfiguration ()Lorg/gradle/api/file/ConfigurableFileCollection; + public final fun getReports ()Lorg/gradle/api/provider/MapProperty; + public final fun getRuleSetsClasspath ()Lorg/gradle/api/file/ConfigurableFileCollection; + public final fun getSource ()Lorg/gradle/api/file/FileCollection; + public final fun getWorkerMaxHeapSize ()Lorg/gradle/api/provider/Property; + public fun include (Lgroovy/lang/Closure;)Lorg/gradle/api/tasks/util/PatternFilterable; + public fun include (Ljava/lang/Iterable;)Lorg/gradle/api/tasks/util/PatternFilterable; + public fun include (Lorg/gradle/api/specs/Spec;)Lorg/gradle/api/tasks/util/PatternFilterable; + public fun include ([Ljava/lang/String;)Lorg/gradle/api/tasks/util/PatternFilterable; + public fun setExcludes (Ljava/lang/Iterable;)Lorg/gradle/api/tasks/util/PatternFilterable; + public fun setIncludes (Ljava/lang/Iterable;)Lorg/gradle/api/tasks/util/PatternFilterable; + public final fun setSource (Ljava/lang/Object;)V + public final fun source ([Ljava/lang/Object;)Lio/github/usefulness/tasks/KtlintWorkTask; +} + +public class io/github/usefulness/tasks/LintTask : io/github/usefulness/tasks/KtlintWorkTask { + public fun (Lorg/gradle/workers/WorkerExecutor;Lorg/gradle/api/model/ObjectFactory;Lorg/gradle/api/file/ProjectLayout;)V + public final fun run (Lorg/gradle/work/InputChanges;)V +} + diff --git a/ktlint-gradle-plugin/build.gradle b/ktlint-gradle-plugin/build.gradle index 76229e7..541d3b1 100644 --- a/ktlint-gradle-plugin/build.gradle +++ b/ktlint-gradle-plugin/build.gradle @@ -1,12 +1,12 @@ -import kotlin.Suppress +import org.jetbrains.kotlin.gradle.dsl.KotlinVersion import org.jetbrains.kotlin.gradle.tasks.KotlinCompile -import org.jetbrains.kotlin.gradle.plugin.KotlinPluginWrapperKt plugins { id("java-gradle-plugin") id("com.starter.publishing") alias(libs.plugins.gradle.pluginpublish) alias(libs.plugins.starter.library.kotlin) + alias(libs.plugins.kotlinx.binarycompatibilityvalidator) } description = "Lint and formatting for Kotlin using ktlint with configuration-free setup on JVM and Android projects" @@ -65,9 +65,9 @@ tasks.named("processResources") { } tasks.withType(KotlinCompile).configureEach { - kotlinOptions { - apiVersion = "1.8" - languageVersion = "1.8" + compilerOptions { + apiVersion = KotlinVersion.KOTLIN_1_8 + languageVersion = KotlinVersion.KOTLIN_1_8 } } tasks.withType(Test).configureEach { diff --git a/ktlint-gradle-plugin/src/main/kotlin/io/github/usefulness/EditorConfigValidationMode.kt b/ktlint-gradle-plugin/src/main/kotlin/io/github/usefulness/EditorConfigValidationMode.kt new file mode 100644 index 0000000..2ebdb73 --- /dev/null +++ b/ktlint-gradle-plugin/src/main/kotlin/io/github/usefulness/EditorConfigValidationMode.kt @@ -0,0 +1,10 @@ +package io.github.usefulness + +import org.gradle.api.Incubating + +@Incubating +public enum class EditorConfigValidationMode { + None, + PrintWarningLogs, + BuildFailure, +} diff --git a/ktlint-gradle-plugin/src/main/kotlin/io/github/usefulness/KtlintGradleExtension.kt b/ktlint-gradle-plugin/src/main/kotlin/io/github/usefulness/KtlintGradleExtension.kt index 0ba148d..df6ec65 100644 --- a/ktlint-gradle-plugin/src/main/kotlin/io/github/usefulness/KtlintGradleExtension.kt +++ b/ktlint-gradle-plugin/src/main/kotlin/io/github/usefulness/KtlintGradleExtension.kt @@ -1,5 +1,6 @@ package io.github.usefulness +import io.github.usefulness.EditorConfigValidationMode.PrintWarningLogs import io.github.usefulness.support.versionProperties import io.github.usefulness.tasks.listProperty import io.github.usefulness.tasks.property @@ -42,4 +43,13 @@ public open class KtlintGradleExtension internal constructor( @Deprecated(message = "Will be removed in the next version", replaceWith = ReplaceWith(expression = "ignoreFilesUnderBuildDir")) @Incubating public val ignoreKspGeneratedSources: Property = ignoreFilesUnderBuildDir + + @Incubating + public val editorConfigValidation: Property = objectFactory.property(default = PrintWarningLogs) + + @Incubating + public fun editorConfigValidation(any: Any) { + val value = EditorConfigValidationMode.values().firstOrNull { it.name.equals(any.toString(), ignoreCase = true) } + editorConfigValidation.set(checkNotNull(value) { "Has to be one of ${EditorConfigValidationMode.values()}, was=$any" }) + } } diff --git a/ktlint-gradle-plugin/src/main/kotlin/io/github/usefulness/KtlintGradlePlugin.kt b/ktlint-gradle-plugin/src/main/kotlin/io/github/usefulness/KtlintGradlePlugin.kt index 8e053fb..cea9644 100644 --- a/ktlint-gradle-plugin/src/main/kotlin/io/github/usefulness/KtlintGradlePlugin.kt +++ b/ktlint-gradle-plugin/src/main/kotlin/io/github/usefulness/KtlintGradlePlugin.kt @@ -1,8 +1,10 @@ package io.github.usefulness +import io.github.usefulness.EditorConfigValidationMode.None import io.github.usefulness.pluginapplier.AndroidSourceSetApplier import io.github.usefulness.pluginapplier.KotlinSourceSetApplier import io.github.usefulness.support.ReporterType +import io.github.usefulness.tasks.CheckEditorConfigTask import io.github.usefulness.tasks.FormatTask import io.github.usefulness.tasks.KtlintWorkTask import io.github.usefulness.tasks.LintTask @@ -32,6 +34,14 @@ public class KtlintGradlePlugin : Plugin { val ktlintConfiguration = createKtlintConfiguration(pluginExtension) val ruleSetConfiguration = createRuleSetConfiguration(ktlintConfiguration) val reportersConfiguration = createReportersConfiguration(ktlintConfiguration) + val recognisedEditorConfigs = generateSequence(project) { it.parent } + .map { it.layout.projectDirectory.file(".editorconfig").asFile } + .toList() + + tasks.register("validateEditorConfigForKtlint", CheckEditorConfigTask::class.java) { + it.editorConfigFiles.from(recognisedEditorConfigs) + it.mode.set(pluginExtension.editorConfigValidation) + } extendablePlugins.forEach { (pluginId, sourceResolver) -> pluginManager.withPlugin(pluginId) { @@ -43,6 +53,7 @@ public class KtlintGradlePlugin : Plugin { task.ruleSetsClasspath.setFrom(ruleSetConfiguration) task.reportersConfiguration.setFrom(reportersConfiguration) task.chunkSize.set(pluginExtension.chunkSize) + task.editorConfigFiles.from(recognisedEditorConfigs) } sourceResolver.applyToAll(this, pluginExtension) { id, resolvedSources -> @@ -87,6 +98,15 @@ public class KtlintGradlePlugin : Plugin { formatKotlin.configure { it.dependsOn(formatWorker) } } + + tasks.named("lintKotlin") { task -> + if (pluginExtension.editorConfigValidation.get() == None) return@named + task.dependsOn("validateEditorConfigForKtlint") + } + tasks.named("formatKotlin") { task -> + if (pluginExtension.editorConfigValidation.get() == None) return@named + task.dependsOn("validateEditorConfigForKtlint") + } } } } diff --git a/ktlint-gradle-plugin/src/main/kotlin/io/github/usefulness/support/EditorConfigUtils.kt b/ktlint-gradle-plugin/src/main/kotlin/io/github/usefulness/support/EditorConfigUtils.kt index 2ce95e3..6f0c62f 100644 --- a/ktlint-gradle-plugin/src/main/kotlin/io/github/usefulness/support/EditorConfigUtils.kt +++ b/ktlint-gradle-plugin/src/main/kotlin/io/github/usefulness/support/EditorConfigUtils.kt @@ -9,7 +9,6 @@ import org.ec4j.core.model.Property import org.ec4j.core.model.PropertyType import org.ec4j.core.model.PropertyType.PropertyValueParser.IDENTITY_VALUE_PARSER import org.ec4j.core.model.Section -import org.gradle.api.file.ProjectLayout import java.io.File internal fun editorConfigOverride(disabledRules: List) = getPropertiesForDisabledRules(disabledRules) @@ -69,19 +68,7 @@ private fun getKtlintRulePropertyName(ruleName: String) = if (ruleName.contains( "ktlint_standard_$ruleName" } -internal fun ProjectLayout.findApplicableEditorConfigFiles(): Sequence { - val projectEditorConfig = projectDirectory.file(".editorconfig").asFile - - return generateSequence(seed = projectEditorConfig) { editorconfig -> - if (editorconfig.isRootEditorConfig) { - null - } else { - editorconfig.parentFile?.parentFile?.resolve(".editorconfig") - } - } -} - -private val File.isRootEditorConfig: Boolean +internal val File.isRootEditorConfig: Boolean get() { if (!isFile || !canRead()) return false diff --git a/ktlint-gradle-plugin/src/main/kotlin/io/github/usefulness/tasks/CheckEditorConfigTask.kt b/ktlint-gradle-plugin/src/main/kotlin/io/github/usefulness/tasks/CheckEditorConfigTask.kt new file mode 100644 index 0000000..d65ffc2 --- /dev/null +++ b/ktlint-gradle-plugin/src/main/kotlin/io/github/usefulness/tasks/CheckEditorConfigTask.kt @@ -0,0 +1,56 @@ +package io.github.usefulness.tasks + +import io.github.usefulness.EditorConfigValidationMode +import io.github.usefulness.EditorConfigValidationMode.BuildFailure +import io.github.usefulness.EditorConfigValidationMode.None +import io.github.usefulness.EditorConfigValidationMode.PrintWarningLogs +import io.github.usefulness.support.isRootEditorConfig +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import javax.inject.Inject + +@CacheableTask +public open class CheckEditorConfigTask @Inject constructor(objectFactory: ObjectFactory) : DefaultTask() { + + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + internal val editorConfigFiles = objectFactory.fileCollection() + + @Input + public val mode: Property = objectFactory.property(default = null) + + @get:OutputFile + internal val resultsFile = project.layout.buildDirectory.file("reports/ktlintValidation/result.txt") + + @TaskAction + public fun run() { + val files = editorConfigFiles.files + val messageFn = { + "None of the recognised `.editorconfig` files contain `root=true` entry, this may result in non-deterministic builds. " + + "Please add `root=true` entry to the top-most editorconfig file\n" + + "`.editorconfig` files:\n${files.joinToString(separator = "\n")}" + } + if (files.none { it.isRootEditorConfig }) { + resultsFile.get().asFile.writeText("Failure") + when (mode.get()) { + None -> Unit + PrintWarningLogs -> logger.warn(messageFn()) + + BuildFailure, + null, + -> throw GradleException(messageFn()) + } + } else { + resultsFile.get().asFile.writeText("OK") + } + } +} diff --git a/ktlint-gradle-plugin/src/main/kotlin/io/github/usefulness/tasks/KtlintWorkTask.kt b/ktlint-gradle-plugin/src/main/kotlin/io/github/usefulness/tasks/KtlintWorkTask.kt index 93939ea..d60bfe3 100644 --- a/ktlint-gradle-plugin/src/main/kotlin/io/github/usefulness/tasks/KtlintWorkTask.kt +++ b/ktlint-gradle-plugin/src/main/kotlin/io/github/usefulness/tasks/KtlintWorkTask.kt @@ -5,7 +5,6 @@ import io.github.usefulness.KtlintGradleExtension.Companion.DEFAULT_DISABLED_RUL import io.github.usefulness.KtlintGradleExtension.Companion.DEFAULT_EXPERIMENTAL_RULES import io.github.usefulness.KtlintGradleExtension.Companion.DEFAULT_IGNORE_FAILURES import io.github.usefulness.support.KtlintRunMode -import io.github.usefulness.support.findApplicableEditorConfigFiles import io.github.usefulness.tasks.workers.ConsoleReportWorker import io.github.usefulness.tasks.workers.GenerateReportsWorker import io.github.usefulness.tasks.workers.KtlintWorker @@ -70,9 +69,7 @@ public abstract class KtlintWorkTask( @get:InputFiles @get:PathSensitive(PathSensitivity.RELATIVE) @get:Incremental - internal val editorconfigFiles = objectFactory.fileCollection().apply { - from(projectLayout.findApplicableEditorConfigFiles().toList()) - } + internal val editorConfigFiles = objectFactory.fileCollection() @Input public val workerMaxHeapSize: Property = objectFactory.property(default = "256m") @@ -192,10 +189,10 @@ internal inline fun ObjectFactory.mapProperty(default: Ma } internal fun KtlintWorkTask.getChangedEditorconfigFiles(inputChanges: InputChanges) = - inputChanges.getFileChanges(editorconfigFiles).map(FileChange::getFile) + inputChanges.getFileChanges(editorConfigFiles).map(FileChange::getFile) internal fun KtlintWorkTask.getChangedSources(inputChanges: InputChanges) = - if (inputChanges.isIncremental && inputChanges.getFileChanges(editorconfigFiles).none()) { + if (inputChanges.isIncremental && inputChanges.getFileChanges(editorConfigFiles).none()) { inputChanges.getFileChanges(source) .asSequence() .filter { it.fileType == FileType.FILE && it.changeType != ChangeType.REMOVED } diff --git a/ktlint-gradle-plugin/src/test/kotlin/io/github/usefulness/functional/EditorConfigTest.kt b/ktlint-gradle-plugin/src/test/kotlin/io/github/usefulness/functional/EditorConfigTest.kt index 163a317..fdc883f 100644 --- a/ktlint-gradle-plugin/src/test/kotlin/io/github/usefulness/functional/EditorConfigTest.kt +++ b/ktlint-gradle-plugin/src/test/kotlin/io/github/usefulness/functional/EditorConfigTest.kt @@ -137,10 +137,12 @@ internal class EditorConfigTest : WithGradleTest.Kotlin() { build("lintKotlin", "--info").apply { assertThat(task(":lintKotlinMain")?.outcome).isEqualTo(TaskOutcome.SUCCESS) assertThat(output).contains("resetting KtLint caches") + assertThat(output).contains("Calculating task graph as no cached configuration is available for tasks: lintKotlin") } build("lintKotlin", "--info").apply { assertThat(task(":lintKotlinMain")?.outcome).isEqualTo(TaskOutcome.UP_TO_DATE) assertThat(output).doesNotContain("resetting KtLint caches") + assertThat(output).contains("Reusing configuration cache.") } projectRoot.resolve(".editorconfig") { @@ -148,6 +150,7 @@ internal class EditorConfigTest : WithGradleTest.Kotlin() { } buildAndFail("lintKotlin", "--info").apply { assertThat(task(":lintKotlinMain")?.outcome).isEqualTo(TaskOutcome.FAILED) + assertThat(output).contains("Reusing configuration cache.") assertThat(output).contains("[standard:filename] File 'FileName.kt' contains a single top level declaration") assertThat(output).contains("resetting KtLint caches") } @@ -158,10 +161,12 @@ internal class EditorConfigTest : WithGradleTest.Kotlin() { build("lintKotlin", "--info").apply { assertThat(task(":lintKotlinMain")?.outcome).isEqualTo(TaskOutcome.SUCCESS) assertThat(output).contains("resetting KtLint caches") + assertThat(output).contains("Reusing configuration cache.") } build("lintKotlin", "--info").apply { assertThat(task(":lintKotlinMain")?.outcome).isEqualTo(TaskOutcome.UP_TO_DATE) assertThat(output).doesNotContain("resetting KtLint caches") + assertThat(output).contains("Reusing configuration cache.") } projectRoot.resolve("src/main/kotlin/AnotherFile.kt") { @@ -170,6 +175,7 @@ internal class EditorConfigTest : WithGradleTest.Kotlin() { build("lintKotlin", "--info").apply { assertThat(task(":lintKotlinMain")?.outcome).isEqualTo(TaskOutcome.SUCCESS) assertThat(output).doesNotContain("resetting KtLint caches") + assertThat(output).contains("Reusing configuration cache.") } } @@ -193,6 +199,8 @@ internal class EditorConfigTest : WithGradleTest.Kotlin() { writeText( // language=editorconfig """ + root = true + [*.{kt,kts}] ktlint_standard_filename = disabled """.trimIndent(), @@ -208,6 +216,8 @@ internal class EditorConfigTest : WithGradleTest.Kotlin() { writeText( // language=editorconfig """ + root = true + [*.{kt,kts}] indent_size = 2 """.trimIndent(), diff --git a/ktlint-gradle-plugin/src/test/kotlin/io/github/usefulness/functional/EditorConfigValidationTest.kt b/ktlint-gradle-plugin/src/test/kotlin/io/github/usefulness/functional/EditorConfigValidationTest.kt new file mode 100644 index 0000000..fbe83b5 --- /dev/null +++ b/ktlint-gradle-plugin/src/test/kotlin/io/github/usefulness/functional/EditorConfigValidationTest.kt @@ -0,0 +1,222 @@ +package io.github.usefulness.functional + +import org.gradle.testkit.runner.TaskOutcome +import io.github.usefulness.functional.utils.editorConfig +import io.github.usefulness.functional.utils.resolve +import io.github.usefulness.functional.utils.settingsFile +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.io.File + +internal class EditorConfigValidationTest : WithGradleTest.Kotlin() { + + lateinit var projectRoot: File + + @BeforeEach + fun setUp() { + projectRoot = testProjectDir.apply { + resolve("settings.gradle") { writeText(settingsFile) } + resolve("build.gradle") { + // language=groovy + val buildScript = + """ + plugins { + id 'kotlin' + id 'io.github.usefulness.ktlint-gradle-plugin' + } + + repositories { + mavenCentral() + } + """.trimIndent() + writeText(buildScript) + } + resolve("src/main/kotlin/Foo.kt") { + // language=kotlin + val content = + """ + object Foo { + fun bar() = 2 + } + + """.trimIndent() + + writeText(content) + } + } + } + + @Test + fun `lint - valid pass and cacheability`() { + projectRoot.resolve(".editorconfig") { + writeText(editorConfig) + } + + build("lintKotlin").apply { + assertThat(task(":validateEditorConfigForKtlint")?.outcome).isEqualTo(TaskOutcome.SUCCESS) + assertThat(task(":lintKotlinMain")?.outcome).isEqualTo(TaskOutcome.SUCCESS) + assertThat(output).doesNotContain(failureMessage) + } + + build("lintKotlin").apply { + assertThat(task(":validateEditorConfigForKtlint")?.outcome).isEqualTo(TaskOutcome.UP_TO_DATE) + assertThat(task(":lintKotlinMain")?.outcome).isEqualTo(TaskOutcome.UP_TO_DATE) + assertThat(output).doesNotContain(failureMessage) + } + + projectRoot.resolve(".editorconfig") { + writeText( + """ + root = true + """.trimIndent(), + ) + } + build("lintKotlin").apply { + assertThat(task(":validateEditorConfigForKtlint")?.outcome).isEqualTo(TaskOutcome.SUCCESS) + assertThat(task(":lintKotlinMain")?.outcome).isEqualTo(TaskOutcome.SUCCESS) + assertThat(output).doesNotContain(failureMessage) + assertThat(output).contains("Reusing configuration cache.") + } + } + + @Test + fun `format - valid pass and cacheability`() { + projectRoot.resolve(".editorconfig") { + writeText(editorConfig) + } + + build("formatKotlin").apply { + assertThat(task(":validateEditorConfigForKtlint")?.outcome).isEqualTo(TaskOutcome.SUCCESS) + assertThat(task(":formatKotlinMain")?.outcome).isEqualTo(TaskOutcome.SUCCESS) + assertThat(output).doesNotContain(failureMessage) + } + + build("formatKotlin").apply { + assertThat(task(":validateEditorConfigForKtlint")?.outcome).isEqualTo(TaskOutcome.UP_TO_DATE) + assertThat(task(":formatKotlinMain")?.outcome).isEqualTo(TaskOutcome.UP_TO_DATE) + assertThat(output).doesNotContain(failureMessage) + } + } + + @Test + fun `default validation`() { + projectRoot.resolve(".editorconfig") { + writeText(invalidEditorConfig) + } + build("lintKotlin").apply { + assertThat(task(":lintKotlinMain")?.outcome).isEqualTo(TaskOutcome.SUCCESS) + assertThat(task(":validateEditorConfigForKtlint")?.outcome).isEqualTo(TaskOutcome.SUCCESS) + assertThat(output).contains(failureMessage) + } + } + + @Test + fun `disabled validation`() { + projectRoot.resolve("build.gradle") { + // language=groovy + appendText( + """ + + ktlint { + editorConfigValidation "None" + } + """.trimIndent(), + ) + } + + build("lintKotlin").apply { + assertThat(task(":lintKotlinMain")?.outcome).isEqualTo(TaskOutcome.SUCCESS) + assertThat(task(":validateEditorConfigForKtlint")).isNull() + assertThat(output).doesNotContain(failureMessage) + } + + projectRoot.resolve("build.gradle") { + // language=groovy + appendText( + """ + + tasks.named("validateEditorConfigForKtlint") { + mode = io.github.usefulness.EditorConfigValidationMode.BuildFailure + } + """.trimIndent(), + ) + } + + buildAndFail("validateEditorConfigForKtlint").apply { + assertThat(task(":validateEditorConfigForKtlint")?.outcome).isEqualTo(TaskOutcome.FAILED) + assertThat(output).contains(failureMessage) + } + build("lintKotlin").apply { + assertThat(task(":lintKotlinMain")?.outcome).isEqualTo(TaskOutcome.UP_TO_DATE) + assertThat(task(":validateEditorConfigForKtlint")).isNull() + assertThat(output).doesNotContain(failureMessage) + } + } + + @Test + fun `validation with warnings`() { + projectRoot.resolve("build.gradle") { + // language=groovy + appendText( + """ + + ktlint { + editorConfigValidation "PrintWarningLogs" + } + """.trimIndent(), + ) + } + build("lintKotlin").apply { + assertThat(task(":lintKotlinMain")?.outcome).isEqualTo(TaskOutcome.SUCCESS) + assertThat(task(":validateEditorConfigForKtlint")?.outcome).isEqualTo(TaskOutcome.SUCCESS) + assertThat(output).contains(failureMessage) + } + + projectRoot.resolve(".editorconfig") { + writeText(editorConfig) + } + build("lintKotlin").apply { + assertThat(task(":lintKotlinMain")?.outcome).isEqualTo(TaskOutcome.SUCCESS) + assertThat(task(":validateEditorConfigForKtlint")?.outcome).isEqualTo(TaskOutcome.SUCCESS) + assertThat(output).doesNotContain(failureMessage) + } + } + + @Test + fun `validation with errors`() { + projectRoot.resolve("build.gradle") { + // language=groovy + appendText( + """ + + ktlint { + editorConfigValidation "BuildFailure" + } + """.trimIndent(), + ) + } + buildAndFail("lintKotlin").apply { + assertThat(task(":lintKotlinMain")?.outcome).isEqualTo(TaskOutcome.SUCCESS) + assertThat(task(":validateEditorConfigForKtlint")?.outcome).isEqualTo(TaskOutcome.FAILED) + assertThat(output).contains(failureMessage) + } + + projectRoot.resolve(".editorconfig") { + writeText(editorConfig) + } + build("lintKotlin").apply { + assertThat(task(":lintKotlinMain")?.outcome).isEqualTo(TaskOutcome.SUCCESS) + assertThat(task(":validateEditorConfigForKtlint")?.outcome).isEqualTo(TaskOutcome.SUCCESS) + assertThat(output).doesNotContain(failureMessage) + } + } + + private val failureMessage = "None of the recognised `.editorconfig` files contain `root=true` entry" + + private val invalidEditorConfig = + """ + [*.kt] + + """.trimIndent() +} diff --git a/ktlint-gradle-plugin/src/test/kotlin/io/github/usefulness/functional/ThirdPartyPlugins.kt b/ktlint-gradle-plugin/src/test/kotlin/io/github/usefulness/functional/ThirdPartyPlugins.kt index 46203d3..51198c6 100644 --- a/ktlint-gradle-plugin/src/test/kotlin/io/github/usefulness/functional/ThirdPartyPlugins.kt +++ b/ktlint-gradle-plugin/src/test/kotlin/io/github/usefulness/functional/ThirdPartyPlugins.kt @@ -30,8 +30,8 @@ class ThirdPartyPlugins : WithGradleTest.Android() { repositories.mavenCentral() dependencies { - implementation "com.google.dagger:dagger:2.48.1" - ksp "com.google.dagger:dagger-compiler:2.48.1" + implementation "com.google.dagger:dagger:2.51" + ksp "com.google.dagger:dagger-compiler:2.51" } @@ -75,6 +75,7 @@ class ThirdPartyPlugins : WithGradleTest.Android() { val result = build("lintKotlin") assertThat(result.tasks.map { it.path }).containsExactlyInAnyOrder( + ":validateEditorConfigForKtlint", ":lintKotlinTest", ":lintKotlinMain", ":lintKotlin", @@ -96,6 +97,7 @@ class ThirdPartyPlugins : WithGradleTest.Android() { assertThat(onlyMain.tasks.map { it.path }).containsAll( listOf( + ":validateEditorConfigForKtlint", ":kspKotlin", ":compileKotlin", ":compileJava", @@ -133,7 +135,7 @@ class ThirdPartyPlugins : WithGradleTest.Android() { repositories.mavenCentral() dependencies { - ksp "com.google.dagger:dagger-compiler:2.48.1" + ksp "com.google.dagger:dagger-compiler:2.51" } """.trimIndent(), @@ -147,6 +149,7 @@ class ThirdPartyPlugins : WithGradleTest.Android() { val result = build("lintKotlin") assertThat(result.tasks.map { it.path }).containsExactlyInAnyOrder( + ":validateEditorConfigForKtlint", ":lintKotlinTestFixturesRelease", ":lintKotlinTestDebug", ":lintKotlinAndroidTest", @@ -178,6 +181,7 @@ class ThirdPartyPlugins : WithGradleTest.Android() { val onlyMain = build("lintKotlin") assertThat(onlyMain.tasks.map { it.path }).containsExactlyInAnyOrder( + ":validateEditorConfigForKtlint", ":lintKotlinTestFixturesRelease", ":lintKotlinTestDebug", ":lintKotlinAndroidTest",