From 64c91f32e9d133d6c6c2d5f09df66e09685af53d Mon Sep 17 00:00:00 2001 From: wakingrufus Date: Wed, 20 Dec 2023 07:13:20 -0600 Subject: [PATCH] fix detection of android kotlin source directories in AGP >= 7 increase maximum gradle version to 8.5 increase maximum ktlint version to 1.1.0 increase maximum kotlin version to 1.9.21 fixes #702 based on work in #703 --- CHANGELOG.md | 2 + README.md | 2 + plugin/VERSION_LATEST_RELEASE.txt | 2 +- plugin/build.gradle.kts | 5 ++ .../jlleitschuh/gradle/ktlint/KtlintPlugin.kt | 2 +- .../ktlint/android/AndroidPluginsApplier.kt | 44 ++++----- .../kotlin/org/assertj/core/api/Assertions.kt | 10 +++ .../ktlint/KtLintSupportedVersionsTest.kt | 3 +- .../gradle/ktlint/UnsupportedGradleTest.kt | 2 +- .../ktlint/android/KtlintPluginAndroidTest.kt | 89 +++++++++++++++++++ .../gradle/ktlint/testdsl/AndroidSetup.kt | 76 ++++++++++++++++ .../gradle/ktlint/testdsl/TestAnnotations.kt | 6 +- .../gradle/ktlint/testdsl/TestDsl.kt | 4 +- settings.gradle.kts | 10 +-- 14 files changed, 215 insertions(+), 42 deletions(-) create mode 100644 plugin/src/test/kotlin/org/assertj/core/api/Assertions.kt create mode 100644 plugin/src/test/kotlin/org/jlleitschuh/gradle/ktlint/android/KtlintPluginAndroidTest.kt create mode 100644 plugin/src/test/kotlin/org/jlleitschuh/gradle/ktlint/testdsl/AndroidSetup.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 518e8b37..7447f2ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] +- fix detection of android kotlin source directories in AGP >= 7 [#733](https://github.com/JLLeitschuh/ktlint-gradle/pull/733) + ## [12.0.3] - 2023-12-11 - fix: apply configuration for source sets and targets that are added after the plugin is diff --git a/README.md b/README.md index 997912dd..a2361d22 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,8 @@ Minimal supported [Kotlin](https://kotlinlang.org) version: `1.4` Minimal supported [ktlint](https://github.com/pinterest/ktlint) version: `0.47.1` +Minimal supported [Android Gradle plugin](https://developer.android.com/build) version: `4.1.0` + ### Ktlint plugin #### Simple setup diff --git a/plugin/VERSION_LATEST_RELEASE.txt b/plugin/VERSION_LATEST_RELEASE.txt index 1a31377d..470ce40f 100644 --- a/plugin/VERSION_LATEST_RELEASE.txt +++ b/plugin/VERSION_LATEST_RELEASE.txt @@ -1 +1 @@ -11.6.1 +12.0.3 diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts index eab4e7f6..9daa5641 100644 --- a/plugin/build.gradle.kts +++ b/plugin/build.gradle.kts @@ -195,6 +195,11 @@ tasks.withType { maxFailures.set(10) } } + + // AGP requires java 17, so set it to 17 for tests + javaLauncher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(17) + } } val relocateShadowJar = tasks.register("relocateShadowJar") diff --git a/plugin/src/main/kotlin/org/jlleitschuh/gradle/ktlint/KtlintPlugin.kt b/plugin/src/main/kotlin/org/jlleitschuh/gradle/ktlint/KtlintPlugin.kt index b5151ec9..8699412c 100644 --- a/plugin/src/main/kotlin/org/jlleitschuh/gradle/ktlint/KtlintPlugin.kt +++ b/plugin/src/main/kotlin/org/jlleitschuh/gradle/ktlint/KtlintPlugin.kt @@ -29,7 +29,7 @@ open class KtlintPlugin : Plugin { private fun PluginHolder.addKtLintTasksToKotlinPlugin() { target.plugins.withId("kotlin", applyKtLint()) target.plugins.withId("org.jetbrains.kotlin.js", applyKtLint()) - target.plugins.withId("kotlin-android", applyKtLintToAndroid()) + target.plugins.withId("org.jetbrains.kotlin.android", applyKtLintToAndroid()) target.plugins.withId( "org.jetbrains.kotlin.multiplatform", applyKtlintMultiplatform() diff --git a/plugin/src/main/kotlin/org/jlleitschuh/gradle/ktlint/android/AndroidPluginsApplier.kt b/plugin/src/main/kotlin/org/jlleitschuh/gradle/ktlint/android/AndroidPluginsApplier.kt index c8c2d7ec..6130cb31 100644 --- a/plugin/src/main/kotlin/org/jlleitschuh/gradle/ktlint/android/AndroidPluginsApplier.kt +++ b/plugin/src/main/kotlin/org/jlleitschuh/gradle/ktlint/android/AndroidPluginsApplier.kt @@ -1,14 +1,8 @@ package org.jlleitschuh.gradle.ktlint.android +import com.android.build.api.dsl.AndroidSourceDirectorySet import com.android.build.api.dsl.AndroidSourceSet -import com.android.build.api.dsl.BuildFeatures -import com.android.build.api.dsl.BuildType import com.android.build.api.dsl.CommonExtension -import com.android.build.api.dsl.DefaultConfig -import com.android.build.api.dsl.ProductFlavor -import com.android.build.api.dsl.SigningConfig -import com.android.build.api.variant.Variant -import com.android.build.api.variant.VariantProperties import com.android.build.gradle.internal.api.DefaultAndroidSourceDirectorySet import org.gradle.api.Plugin import org.gradle.api.file.FileCollection @@ -21,6 +15,7 @@ import org.jlleitschuh.gradle.ktlint.createGenerateReportsTask import org.jlleitschuh.gradle.ktlint.setCheckTaskDependsOnGenerateReportsTask import org.jlleitschuh.gradle.ktlint.tasks.GenerateReportsTask import java.util.concurrent.Callable +import kotlin.reflect.full.memberProperties internal fun KtlintPlugin.PluginHolder.applyKtLintToAndroid(): (Plugin) -> Unit { return { @@ -43,33 +38,32 @@ internal fun KtlintPlugin.PluginHolder.applyKtLintToAndroid(): (Plugin) } } -@Suppress("UnstableApiUsage") -private typealias AndroidCommonExtension = CommonExtension< - AndroidSourceSet, - BuildFeatures, - BuildType, - DefaultConfig, - ProductFlavor, - SigningConfig, - Variant, - VariantProperties - > - /* * Variant manager returns all sources for variant, * so most probably main source set maybe checked several times. * This approach creates one check tasks per one source set. */ -@Suppress("UNCHECKED_CAST", "UnstableApiUsage") +@Suppress("UnstableApiUsage") private fun androidPluginConfigureAction( pluginHolder: KtlintPlugin.PluginHolder ): (Plugin) -> Unit = { pluginHolder.target.extensions.configure(CommonExtension::class.java) { - val androidCommonExtension = this as AndroidCommonExtension - - androidCommonExtension.sourceSets.all { - // https://issuetracker.google.com/u/1/issues/170650362 - val androidSourceSet = java as DefaultAndroidSourceDirectorySet + // kotlin property exists in AGP >= 7 + val kotlinProperty = AndroidSourceSet::class.memberProperties.firstOrNull { it.name == "kotlin" } + if (kotlinProperty == null) { + pluginHolder.target.logger.warn( + buildString { + append("In AGP <7 kotlin source directories are not auto-detected.") + append("In order to lint kotlin sources, manually add the directory to the source set. ") + append("""For example: sourceSets.getByName("main").java.srcDirs("src/main/kotlin/")""") + } + ) + } + val sourceMember: AndroidSourceSet.() -> AndroidSourceDirectorySet = { + kotlinProperty?.get(this) as AndroidSourceDirectorySet? ?: this.java + } + sourceSets.all { + val androidSourceSet = sourceMember(this) as DefaultAndroidSourceDirectorySet // Passing Callable, so returned FileCollection, will lazy evaluate it // only when task will need it. // Solves the problem of having additional source dirs in diff --git a/plugin/src/test/kotlin/org/assertj/core/api/Assertions.kt b/plugin/src/test/kotlin/org/assertj/core/api/Assertions.kt new file mode 100644 index 00000000..72b9eb80 --- /dev/null +++ b/plugin/src/test/kotlin/org/assertj/core/api/Assertions.kt @@ -0,0 +1,10 @@ +package org.assertj.core.api + +import org.assertj.core.internal.Objects +import org.gradle.testkit.runner.BuildTask +import org.gradle.testkit.runner.TaskOutcome + +fun ObjectAssert.hasOutcome(outcome: TaskOutcome) { + Objects.instance().assertNotNull(this.info, this.actual) + Objects.instance().assertEqual(this.info, this.actual!!.outcome, outcome) +} diff --git a/plugin/src/test/kotlin/org/jlleitschuh/gradle/ktlint/KtLintSupportedVersionsTest.kt b/plugin/src/test/kotlin/org/jlleitschuh/gradle/ktlint/KtLintSupportedVersionsTest.kt index 9ecd938e..af283e69 100644 --- a/plugin/src/test/kotlin/org/jlleitschuh/gradle/ktlint/KtLintSupportedVersionsTest.kt +++ b/plugin/src/test/kotlin/org/jlleitschuh/gradle/ktlint/KtLintSupportedVersionsTest.kt @@ -181,7 +181,8 @@ class KtLintSupportedVersionsTest : AbstractPluginTest() { "0.49.1", "0.50.0", "1.0.0", - "1.0.1" + "1.0.1", + "1.1.0" ) override fun provideArguments( diff --git a/plugin/src/test/kotlin/org/jlleitschuh/gradle/ktlint/UnsupportedGradleTest.kt b/plugin/src/test/kotlin/org/jlleitschuh/gradle/ktlint/UnsupportedGradleTest.kt index 13913896..26cf4418 100644 --- a/plugin/src/test/kotlin/org/jlleitschuh/gradle/ktlint/UnsupportedGradleTest.kt +++ b/plugin/src/test/kotlin/org/jlleitschuh/gradle/ktlint/UnsupportedGradleTest.kt @@ -14,7 +14,7 @@ class UnsupportedGradleTest : AbstractPluginTest() { @Test @DisabledOnOs(OS.WINDOWS) internal fun errorOnOldGradleVersion() { - project(GradleVersion.version("6.9.2")) { + project(GradleVersion.version("7.4.1")) { buildAndFail(CHECK_PARENT_TASK_NAME) { assertThat(output).contains( "Current version of plugin supports minimal Gradle version: " + diff --git a/plugin/src/test/kotlin/org/jlleitschuh/gradle/ktlint/android/KtlintPluginAndroidTest.kt b/plugin/src/test/kotlin/org/jlleitschuh/gradle/ktlint/android/KtlintPluginAndroidTest.kt new file mode 100644 index 00000000..b6aba887 --- /dev/null +++ b/plugin/src/test/kotlin/org/jlleitschuh/gradle/ktlint/android/KtlintPluginAndroidTest.kt @@ -0,0 +1,89 @@ +package org.jlleitschuh.gradle.ktlint.android + +import net.swiftzer.semver.SemVer +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.hasOutcome +import org.gradle.testkit.runner.TaskOutcome +import org.gradle.util.GradleVersion +import org.jlleitschuh.gradle.ktlint.AbstractPluginTest +import org.jlleitschuh.gradle.ktlint.CHECK_PARENT_TASK_NAME +import org.jlleitschuh.gradle.ktlint.testdsl.TestVersions +import org.jlleitschuh.gradle.ktlint.testdsl.androidProjectSetup +import org.jlleitschuh.gradle.ktlint.testdsl.build +import org.jlleitschuh.gradle.ktlint.testdsl.project +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource + +class KtlintPluginAndroidTest : AbstractPluginTest() { + + @ParameterizedTest + @EnumSource(AndroidTestInput::class) + fun `ktlint pass src java`(input: AndroidTestInput) { + project( + input.gradleVersion, + projectSetup = androidProjectSetup(input.agpVersion, input.kotlinVersion, input.ktlintVersion) + ) { + withCleanSources("src/main/java/CleanSource.kt") + build(CHECK_PARENT_TASK_NAME) { + assertThat(task(":$mainSourceSetCheckTaskName")).hasOutcome(TaskOutcome.SUCCESS) + assertThat(task(":$kotlinScriptCheckTaskName")).hasOutcome(TaskOutcome.SUCCESS) + assertThat(task(":$CHECK_PARENT_TASK_NAME")).hasOutcome(TaskOutcome.SUCCESS) + } + } + } + + @ParameterizedTest + @EnumSource(AndroidTestInput::class) + fun `ktlint pass src kotlin`(input: AndroidTestInput) { + project( + input.gradleVersion, + projectSetup = androidProjectSetup(input.agpVersion, input.kotlinVersion, input.ktlintVersion) + ) { + withCleanSources("src/main/kotlin/CleanSource.kt") + if (SemVer.parse(input.agpVersion) < SemVer(7)) { + build(CHECK_PARENT_TASK_NAME) { + assertThat(output).contains("In AGP <7 kotlin source directories are not auto-detected.") + assertThat(task(":$mainSourceSetCheckTaskName")).hasOutcome(TaskOutcome.SKIPPED) + assertThat(task(":$kotlinScriptCheckTaskName")).hasOutcome(TaskOutcome.SUCCESS) + assertThat(task(":$CHECK_PARENT_TASK_NAME")).hasOutcome(TaskOutcome.SUCCESS) + } + } else { + build(CHECK_PARENT_TASK_NAME) { + assertThat(task(":$mainSourceSetCheckTaskName")).hasOutcome(TaskOutcome.SUCCESS) + assertThat(task(":$kotlinScriptCheckTaskName")).hasOutcome(TaskOutcome.SUCCESS) + assertThat(task(":$CHECK_PARENT_TASK_NAME")).hasOutcome(TaskOutcome.SUCCESS) + } + } + } + } + + enum class AndroidTestInput( + val gradleVersion: GradleVersion, + val agpVersion: String, + val kotlinVersion: String, + val ktlintVersion: String? =null + ) { + MIN( + GradleVersion.version(TestVersions.minSupportedGradleVersion), + TestVersions.minAgpVersion, + "1.5.20", // AGP 4.1 requires kotlin 1.5.20 + "0.47.1" // old AGP doesn't properly set variant info which ktlint >= 1 requires + ), + GRADLE_7_5_AGP_7_4( + GradleVersion.version("7.5"), // AGP 7.4 requires Gradle 7.5 + "7.4.2", + "1.5.20", // AGP 4.1 requires kotlin 1.5.20 + "0.47.1" // old AGP doesn't properly set variant info which ktlint >= 1 requires + ), + MAX_GRADLE_MIN_AGP( + GradleVersion.version(TestVersions.maxSupportedGradleVersion), + TestVersions.minAgpVersion, + "1.5.20", // AGP 4.1 requires kotlin 1.5.20 + ), + MAX( + GradleVersion.version(TestVersions.maxSupportedGradleVersion), + TestVersions.maxAgpVersion, + TestVersions.maxSupportedKotlinPluginVersion + ) + } +} diff --git a/plugin/src/test/kotlin/org/jlleitschuh/gradle/ktlint/testdsl/AndroidSetup.kt b/plugin/src/test/kotlin/org/jlleitschuh/gradle/ktlint/testdsl/AndroidSetup.kt new file mode 100644 index 00000000..21798489 --- /dev/null +++ b/plugin/src/test/kotlin/org/jlleitschuh/gradle/ktlint/testdsl/AndroidSetup.kt @@ -0,0 +1,76 @@ +package org.jlleitschuh.gradle.ktlint.testdsl + +import net.swiftzer.semver.SemVer +import java.io.File + +fun androidProjectSetup( + agpVersion: String, + kotlinPluginVersion: String, + ktlintVersion: String? = null +): (File) -> Unit = { + val ktLintOverride = ktlintVersion?.let { "ktlint { version = \"$it\" }\n" } ?: "" + val setNamespace = if ((SemVer.parse(agpVersion) >= SemVer(7))) { + " namespace = \"com.example.myapp\"\n" + } else { + "" + } + //language=Groovy + it.resolve("build.gradle").writeText( + """ + |plugins { + | id("com.android.application") + | id("org.jetbrains.kotlin.android") + | id("org.jlleitschuh.gradle.ktlint") + |} + | + |repositories { + | mavenCentral() + |} + |android { + | compileSdk = 33 + |$setNamespace} + |$ktLintOverride + """.trimMargin() + ) + + // before 4.2.0, AGP did not properly publish metadata for id resolution + val oldAgpHack = if ((SemVer.parse(agpVersion) < SemVer(4, 2))) { + """ + | resolutionStrategy { + | eachPlugin { + | when (requested.id.id) { + | "com.android.application" -> useModule("com.android.tools.build:gradle:$agpVersion") + | } + | } + | } + | + """.trimMargin() + } else { + "" + } + val newAgp = if ((SemVer.parse(agpVersion) < SemVer(4, 2))) { + "" + } else { + " id(\"com.android.application\") version \"$agpVersion\"\n " + } + + //language=Groovy + it.resolve("settings.gradle.kts").writeText( + """ + |pluginManagement { + | repositories { + | mavenLocal() + | gradlePluginPortal() + | google() + | mavenCentral() + | } + | + | plugins { + | id("org.jetbrains.kotlin.android") version("$kotlinPluginVersion") + | id("org.jlleitschuh.gradle.ktlint") version("${TestVersions.pluginVersion}") + | $newAgp} + |$oldAgpHack} + | + """.trimMargin() + ) +} diff --git a/plugin/src/test/kotlin/org/jlleitschuh/gradle/ktlint/testdsl/TestAnnotations.kt b/plugin/src/test/kotlin/org/jlleitschuh/gradle/ktlint/testdsl/TestAnnotations.kt index e3416888..134a35ce 100644 --- a/plugin/src/test/kotlin/org/jlleitschuh/gradle/ktlint/testdsl/TestAnnotations.kt +++ b/plugin/src/test/kotlin/org/jlleitschuh/gradle/ktlint/testdsl/TestAnnotations.kt @@ -12,10 +12,12 @@ import kotlin.streams.asStream object TestVersions { const val minSupportedGradleVersion = KtlintBasePlugin.LOWEST_SUPPORTED_GRADLE_VERSION - const val maxSupportedGradleVersion = "8.4" + const val maxSupportedGradleVersion = "8.5" val pluginVersion = File("VERSION_CURRENT.txt").readText().trim() const val minSupportedKotlinPluginVersion = "1.4.32" - const val maxSupportedKotlinPluginVersion = "1.9.10" + const val maxSupportedKotlinPluginVersion = "1.9.21" + const val minAgpVersion = "4.1.0" + const val maxAgpVersion = "8.2.0" } @Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) diff --git a/plugin/src/test/kotlin/org/jlleitschuh/gradle/ktlint/testdsl/TestDsl.kt b/plugin/src/test/kotlin/org/jlleitschuh/gradle/ktlint/testdsl/TestDsl.kt index 2594d3ff..0e4533ef 100644 --- a/plugin/src/test/kotlin/org/jlleitschuh/gradle/ktlint/testdsl/TestDsl.kt +++ b/plugin/src/test/kotlin/org/jlleitschuh/gradle/ktlint/testdsl/TestDsl.kt @@ -40,9 +40,9 @@ class TestProject( val settingsGradle get() = projectPath.resolve("settings.gradle") val editorConfig get() = projectPath.resolve(".editorconfig") - fun withCleanSources() { + fun withCleanSources(filePath: String = CLEAN_SOURCES_FILE) { createSourceFile( - CLEAN_SOURCES_FILE, + filePath, """ |val foo = "bar" | diff --git a/settings.gradle.kts b/settings.gradle.kts index ce826a9b..2dd1e85c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -6,6 +6,7 @@ pluginManagement { plugins { id("org.jetbrains.kotlin.jvm") version "1.8.22" id("org.jetbrains.kotlin.js") version "1.8.22" + id("com.android.application") version "4.2.2" } repositories { @@ -13,15 +14,6 @@ pluginManagement { google() maven("https://dl.bintray.com/jetbrains/kotlin-native-dependencies") } - - resolutionStrategy { - eachPlugin { - when (requested.id.id) { - "com.android.application" -> - useModule("com.android.tools.build:gradle:4.1.3") - } - } - } } enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")