Repository: jeremymailen/kotlinter-gradle (kotlin)

Issue:
LintTask doesn't rerun when editorconfig changes
I have been trying to configure kotlinter with my `.editorconfig` and I was having problems getting `kotlinter` to respect my rules since it looks like the lint task is being persistently cached.

Every time I make a `.editorconfig` change and run `build`, the change gets picked up by kotlinter, however, every time I run `LintTask`, whether right after a gradle clean or with `./gradlew clean lintKotlin` it says there are no issues even though there are lint errors since I changed the `editorconfig` rules.

As a temporary work-around, https://github.com/jeremymailen/kotlinter-gradle/issues/112#issuecomment-521733398 helped me prevent the `ListTask` from caching when I run `./gradlew lintKotlin --rerun-tasks`. 

This issue makes me lose confidence in the `prePushHook` or other linter automation since I will never know if the linter truly should have succeeded, so are there any permanent solutions to this issue?

Pull request description:
With the motivation of reducing the number of opened issues, I'm proposing to fix #233 by adding all _applicable_ `EditorConfig` files as task inputs

Patch:
diff --git a/build.gradle.kts b/build.gradle.kts
index 9f1d58f0..0b4f9651 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -105,7 +105,7 @@ tasks {
     }
 
     wrapper {
-        gradleVersion = "7.4.1"
+        gradleVersion = "7.4.2"
     }
 }
 
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index d7e66b5c..92f06b50 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
diff --git a/src/main/kotlin/org/jmailen/gradle/kotlinter/tasks/ConfigurableKtLintTask.kt b/src/main/kotlin/org/jmailen/gradle/kotlinter/tasks/ConfigurableKtLintTask.kt
index 86e5b46c..fd918c2d 100644
--- a/src/main/kotlin/org/jmailen/gradle/kotlinter/tasks/ConfigurableKtLintTask.kt
+++ b/src/main/kotlin/org/jmailen/gradle/kotlinter/tasks/ConfigurableKtLintTask.kt
@@ -1,24 +1,38 @@
 package org.jmailen.gradle.kotlinter.tasks
 
-import org.gradle.api.DefaultTask
+import org.gradle.api.file.FileCollection
+import org.gradle.api.file.ProjectLayout
+import org.gradle.api.model.ObjectFactory
 import org.gradle.api.provider.ListProperty
 import org.gradle.api.provider.MapProperty
 import org.gradle.api.provider.Property
 import org.gradle.api.tasks.Input
+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.internal.exceptions.MultiCauseException
 import org.jmailen.gradle.kotlinter.KotlinterExtension.Companion.DEFAULT_DISABLED_RULES
 import org.jmailen.gradle.kotlinter.KotlinterExtension.Companion.DEFAULT_EXPERIMENTAL_RULES
 import org.jmailen.gradle.kotlinter.support.KtLintParams
 
-abstract class ConfigurableKtLintTask : SourceTask() {
+abstract class ConfigurableKtLintTask(
+    projectLayout: ProjectLayout,
+    objectFactory: ObjectFactory,
+) : SourceTask() {
 
     @Input
-    val experimentalRules: Property<Boolean> = property(default = DEFAULT_EXPERIMENTAL_RULES)
+    val experimentalRules: Property<Boolean> = objectFactory.property(default = DEFAULT_EXPERIMENTAL_RULES)
 
     @Input
-    val disabledRules: ListProperty<String> = listProperty(default = DEFAULT_DISABLED_RULES.toList())
+    val disabledRules: ListProperty<String> = objectFactory.listProperty(default = DEFAULT_DISABLED_RULES.toList())
+
+    @get:InputFiles
+    @get:PathSensitive(PathSensitivity.RELATIVE)
+    internal val editorconfigFiles: FileCollection = objectFactory.fileCollection().apply {
+        from(projectLayout.findApplicableEditorConfigFiles().toList())
+    }
 
     @Internal
     protected fun getKtLintParams(): KtLintParams = KtLintParams(
@@ -27,18 +41,18 @@ abstract class ConfigurableKtLintTask : SourceTask() {
     )
 }
 
-internal inline fun <reified T> DefaultTask.property(default: T? = null): Property<T> =
-    project.objects.property(T::class.java).apply {
+internal inline fun <reified T> ObjectFactory.property(default: T? = null): Property<T> =
+    property(T::class.java).apply {
         set(default)
     }
 
-internal inline fun <reified T> DefaultTask.listProperty(default: Iterable<T> = emptyList()): ListProperty<T> =
-    project.objects.listProperty(T::class.java).apply {
+internal inline fun <reified T> ObjectFactory.listProperty(default: Iterable<T> = emptyList()): ListProperty<T> =
+    listProperty(T::class.java).apply {
         set(default)
     }
 
-internal inline fun <reified K, reified V> DefaultTask.mapProperty(default: Map<K, V> = emptyMap()): MapProperty<K, V> =
-    project.objects.mapProperty(K::class.java, V::class.java).apply {
+internal inline fun <reified K, reified V> ObjectFactory.mapProperty(default: Map<K, V> = emptyMap()): MapProperty<K, V> =
+    mapProperty(K::class.java, V::class.java).apply {
         set(default)
     }
 
diff --git a/src/main/kotlin/org/jmailen/gradle/kotlinter/tasks/EditorConfigUtils.kt b/src/main/kotlin/org/jmailen/gradle/kotlinter/tasks/EditorConfigUtils.kt
new file mode 100644
index 00000000..343a9eae
--- /dev/null
+++ b/src/main/kotlin/org/jmailen/gradle/kotlinter/tasks/EditorConfigUtils.kt
@@ -0,0 +1,30 @@
+package org.jmailen.gradle.kotlinter.tasks
+
+import org.gradle.api.file.ProjectLayout
+import java.io.File
+
+internal fun ProjectLayout.findApplicableEditorConfigFiles(): Sequence<File> {
+    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
+    get() {
+        if (!isFile || !canRead()) return false
+
+        return useLines { lines ->
+            lines.any { line -> line.matches(editorConfigRootRegex) }
+        }
+    }
+
+/**
+ * According to https://editorconfig.org/ root-most EditorConfig file contains line with `root=true`
+ */
+private val editorConfigRootRegex = "^root\\s?=\\s?true".toRegex()
diff --git a/src/main/kotlin/org/jmailen/gradle/kotlinter/tasks/FormatTask.kt b/src/main/kotlin/org/jmailen/gradle/kotlinter/tasks/FormatTask.kt
index 1ee104d2..cae4f0c6 100644
--- a/src/main/kotlin/org/jmailen/gradle/kotlinter/tasks/FormatTask.kt
+++ b/src/main/kotlin/org/jmailen/gradle/kotlinter/tasks/FormatTask.kt
@@ -1,7 +1,9 @@
 package org.jmailen.gradle.kotlinter.tasks
 
 import org.gradle.api.GradleException
+import org.gradle.api.file.ProjectLayout
 import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.model.ObjectFactory
 import org.gradle.api.tasks.Optional
 import org.gradle.api.tasks.OutputFile
 import org.gradle.api.tasks.TaskAction
@@ -12,14 +14,17 @@ import org.jmailen.gradle.kotlinter.tasks.format.FormatWorkerAction
 import javax.inject.Inject
 
 open class FormatTask @Inject constructor(
-    private val workerExecutor: WorkerExecutor
-) : ConfigurableKtLintTask() {
+    private val workerExecutor: WorkerExecutor,
+    objectFactory: ObjectFactory,
+    private val projectLayout: ProjectLayout,
+) : ConfigurableKtLintTask(
+    projectLayout = projectLayout,
+    objectFactory = objectFactory,
+) {
 
     @OutputFile
     @Optional
-    val report: RegularFileProperty = project.objects.fileProperty()
-
-    private val projectDir = project.projectDir
+    val report: RegularFileProperty = objectFactory.fileProperty()
 
     init {
         outputs.upToDateWhen { false }
@@ -31,7 +36,7 @@ open class FormatTask @Inject constructor(
             submit(FormatWorkerAction::class.java) { p ->
                 p.name.set(name)
                 p.files.from(source)
-                p.projectDirectory.set(projectDir)
+                p.projectDirectory.set(projectLayout.projectDirectory.asFile)
                 p.ktLintParams.set(getKtLintParams())
                 p.output.set(report)
             }
diff --git a/src/main/kotlin/org/jmailen/gradle/kotlinter/tasks/GitHookTasks.kt b/src/main/kotlin/org/jmailen/gradle/kotlinter/tasks/GitHookTasks.kt
index 7800352e..d1d9fdb3 100644
--- a/src/main/kotlin/org/jmailen/gradle/kotlinter/tasks/GitHookTasks.kt
+++ b/src/main/kotlin/org/jmailen/gradle/kotlinter/tasks/GitHookTasks.kt
@@ -34,7 +34,7 @@ open class InstallPrePushHookTask : InstallHookTask("pre-push") {
  */
 abstract class InstallHookTask(@get:Internal val hookFileName: String) : DefaultTask() {
     @Input
-    val gitDirPath: Property<String> = property(default = ".git")
+    val gitDirPath: Property<String> = project.objects.property(default = ".git")
 
     @get:Internal
     abstract val hookContent: String
diff --git a/src/main/kotlin/org/jmailen/gradle/kotlinter/tasks/LintTask.kt b/src/main/kotlin/org/jmailen/gradle/kotlinter/tasks/LintTask.kt
index f52fb79b..c0aa674d 100644
--- a/src/main/kotlin/org/jmailen/gradle/kotlinter/tasks/LintTask.kt
+++ b/src/main/kotlin/org/jmailen/gradle/kotlinter/tasks/LintTask.kt
@@ -2,6 +2,8 @@ package org.jmailen.gradle.kotlinter.tasks
 
 import org.gradle.api.GradleException
 import org.gradle.api.file.FileTree
+import org.gradle.api.file.ProjectLayout
+import org.gradle.api.model.ObjectFactory
 import org.gradle.api.provider.MapProperty
 import org.gradle.api.provider.Property
 import org.gradle.api.tasks.CacheableTask
@@ -22,20 +24,23 @@ import javax.inject.Inject
 
 @CacheableTask
 open class LintTask @Inject constructor(
-    private val workerExecutor: WorkerExecutor
-) : ConfigurableKtLintTask() {
+    private val workerExecutor: WorkerExecutor,
+    objectFactory: ObjectFactory,
+    private val projectLayout: ProjectLayout,
+) : ConfigurableKtLintTask(
+    projectLayout = projectLayout,
+    objectFactory = objectFactory,
+) {
 
     @OutputFiles
-    val reports: MapProperty<String, File> = mapProperty(default = emptyMap())
+    val reports: MapProperty<String, File> = objectFactory.mapProperty(default = emptyMap())
 
     @InputFiles
     @PathSensitive(PathSensitivity.RELATIVE)
     override fun getSource(): FileTree = super.getSource()
 
     @Input
-    val ignoreFailures: Property<Boolean> = property(default = DEFAULT_IGNORE_FAILURES)
-
-    private val projectDir = project.projectDir
+    val ignoreFailures: Property<Boolean> = objectFactory.property(default = DEFAULT_IGNORE_FAILURES)
 
     @TaskAction
     fun run() {
@@ -43,7 +48,7 @@ open class LintTask @Inject constructor(
             submit(LintWorkerAction::class.java) { p ->
                 p.name.set(name)
                 p.files.from(source)
-                p.projectDirectory.set(projectDir)
+                p.projectDirectory.set(projectLayout.projectDirectory.asFile)
                 p.reporters.putAll(reports)
                 p.ktLintParams.set(getKtLintParams())
             }


Test patch:
diff --git a/src/test/kotlin/org/jmailen/gradle/kotlinter/functional/KotlinProjectTest.kt b/src/test/kotlin/org/jmailen/gradle/kotlinter/functional/KotlinProjectTest.kt
index 2655b900..5f0f77e1 100644
--- a/src/test/kotlin/org/jmailen/gradle/kotlinter/functional/KotlinProjectTest.kt
+++ b/src/test/kotlin/org/jmailen/gradle/kotlinter/functional/KotlinProjectTest.kt
@@ -3,6 +3,7 @@ package org.jmailen.gradle.kotlinter.functional
 import org.gradle.testkit.runner.TaskOutcome.FAILED
 import org.gradle.testkit.runner.TaskOutcome.SUCCESS
 import org.gradle.testkit.runner.TaskOutcome.UP_TO_DATE
+import org.jmailen.gradle.kotlinter.functional.utils.editorConfig
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertTrue
 import org.junit.Before
@@ -14,6 +15,7 @@ internal class KotlinProjectTest : WithGradleTest.Kotlin() {
     private lateinit var settingsFile: File
     private lateinit var buildFile: File
     private lateinit var sourceDir: File
+    private lateinit var editorconfigFile: File
     private val pathPattern = "(.*\\.kt):\\d+:\\d+".toRegex()
 
     @Before
@@ -21,6 +23,7 @@ internal class KotlinProjectTest : WithGradleTest.Kotlin() {
         settingsFile = testProjectDir.newFile("settings.gradle")
         buildFile = testProjectDir.newFile("build.gradle")
         sourceDir = testProjectDir.newFolder("src", "main", "kotlin")
+        editorconfigFile = testProjectDir.newFile(".editorconfig")
     }
 
     @Test
@@ -120,6 +123,7 @@ internal class KotlinProjectTest : WithGradleTest.Kotlin() {
     fun `tasks up-to-date checks`() {
         settingsFile()
         buildFile()
+        editorConfig()
         kotlinSourceFile(
             "CustomObject.kt",
             """
@@ -141,6 +145,14 @@ internal class KotlinProjectTest : WithGradleTest.Kotlin() {
         build("formatKotlin").apply {
             assertEquals(SUCCESS, task(":formatKotlin")?.outcome)
         }
+
+        editorconfigFile.appendText("content=updated")
+        build("lintKotlin").apply {
+            assertEquals(SUCCESS, task(":lintKotlin")?.outcome)
+        }
+        build("lintKotlin").apply {
+            assertEquals(UP_TO_DATE, task(":lintKotlin")?.outcome)
+        }
     }
 
     @Test
@@ -178,6 +190,10 @@ internal class KotlinProjectTest : WithGradleTest.Kotlin() {
         writeText("rootProject.name = 'kotlinter'")
     }
 
+    private fun editorConfig() = editorconfigFile.apply {
+        writeText(editorConfig)
+    }
+
     private fun buildFile() = buildFile.apply {
         // language=groovy
         val buildscript =