diff --git a/plugin/src/main/kotlin/org/jlleitschuh/gradle/ktlint/worker/KtLintInvocation.kt b/plugin/src/main/kotlin/org/jlleitschuh/gradle/ktlint/worker/KtLintInvocation.kt new file mode 100644 index 00000000..60293ef9 --- /dev/null +++ b/plugin/src/main/kotlin/org/jlleitschuh/gradle/ktlint/worker/KtLintInvocation.kt @@ -0,0 +1,249 @@ +package org.jlleitschuh.gradle.ktlint.worker + +import com.pinterest.ktlint.core.LintError +import com.pinterest.ktlint.core.api.FeatureInAlphaState +import java.io.File +import kotlin.reflect.full.declaredMemberFunctions +import kotlin.reflect.full.findParameterByName +import kotlin.reflect.full.instanceParameter +import kotlin.reflect.full.memberFunctions +import kotlin.reflect.full.memberProperties +import kotlin.reflect.full.primaryConstructor + +/** + * An abstraction for invoking ktlint across all breaking changes between versions + */ +internal sealed interface KtLintInvocation { + fun invokeLint(file: File, cb: (LintError, Boolean) -> Unit) + fun invokeFormat(file: File, cb: (LintError, Boolean) -> Unit): String +} + +internal class LegacyParamsInvocation( +) : KtLintInvocation { + private var editorConfigPath: String? = null + private var debug: Boolean = false + private lateinit var ruleSets: Set + private lateinit var userData: Map + fun initialize( + editorConfigPath: String?, + ruleSets: Set, + userData: Map, + debug: Boolean + ) { + this.editorConfigPath = editorConfigPath + this.ruleSets = ruleSets + this.userData = userData + this.debug = debug + } + private fun buildParams(file: File, cb: (LintError, Boolean) -> Unit): com.pinterest.ktlint.core.KtLint.Params { + val script = !file.name.endsWith(".kt", ignoreCase = true) + return com.pinterest.ktlint.core.KtLint.Params( + fileName = file.absolutePath, + text = file.readText(), + ruleSets = ruleSets, + userData = userData, + debug = debug, + editorConfigPath = editorConfigPath, + script = script, + cb = cb + ) + } + + override fun invokeLint(file: File, cb: (LintError, Boolean) -> Unit) { + com.pinterest.ktlint.core.KtLint.lint(buildParams(file, cb)) + } + + override fun invokeFormat(file: File, cb: (LintError, Boolean) -> Unit): String { + return com.pinterest.ktlint.core.KtLint.format(buildParams(file, cb)) + } + +} + +@OptIn(FeatureInAlphaState::class) +internal class ExperimentalParamsInvocation : KtLintInvocation { + private var editorConfigPath: String? = null + private var debug: Boolean = false + private lateinit var ruleSets: Set + private lateinit var userData: Map + fun initialize( + editorConfigPath: String?, + ruleSets: Set, + userData: Map, + debug: Boolean + ) { + this.editorConfigPath = editorConfigPath + this.ruleSets = ruleSets + this.userData = userData + this.debug = debug + } + + private fun buildParams(file: File, cb: (LintError, Boolean) -> Unit): com.pinterest.ktlint.core.KtLint.ExperimentalParams { + val script = !file.name.endsWith(".kt", ignoreCase = true) + val ctor = Class.forName("com.pinterest.ktlint.core.KtLint\$ExperimentalParams").kotlin.primaryConstructor + val editorConfigOverride = userDataToEditorConfigOverride(userData) + return ctor!!.callBy( + mapOf( + ctor.findParameterByName("fileName")!! to file.absolutePath, + ctor.findParameterByName("text")!! to file.readText(), + ctor.findParameterByName("ruleSets")!! to ruleSets, + ctor.findParameterByName("cb")!! to cb, + ctor.findParameterByName("script")!! to script, + ctor.findParameterByName("editorConfigPath")!! to editorConfigPath, + ctor.findParameterByName("debug")!! to debug, + ctor.findParameterByName("editorConfigOverride")!! to editorConfigOverride + ) + ) as com.pinterest.ktlint.core.KtLint.ExperimentalParams + } + + override fun invokeLint(file: File, cb: (LintError, Boolean) -> Unit) { + com.pinterest.ktlint.core.KtLint.lint(buildParams(file, cb)) + } + + override fun invokeFormat(file: File, cb: (LintError, Boolean) -> Unit): String { + return com.pinterest.ktlint.core.KtLint.format(buildParams(file, cb)) + } +} + +fun getCodeStyle(styleName: String): Any { + return try { + Class.forName("com.pinterest.ktlint.core.api.DefaultEditorConfigProperties\$CodeStyleValue").getDeclaredField(styleName).get(null) + } catch (e: ClassNotFoundException) { + (Class.forName("com.pinterest.ktlint.core.api.editorconfig.CodeStyleValue").enumConstants as Array>).first { + it.name == styleName + } + } +} + +fun getEditorConfigPropertyClass(): Class<*> { + return try { + Class.forName("com.pinterest.ktlint.core.api.UsesEditorConfigProperties\$EditorConfigProperty") + } catch (e: ClassNotFoundException) { + Class.forName("com.pinterest.ktlint.core.api.editorconfig.EditorConfigProperty") + } +} + +@OptIn(FeatureInAlphaState::class) +fun userDataToEditorConfigOverride(userData: Map, useNewDisabledProp: Boolean = false): Any { + val defaultEditorConfigPropertiesClass = Class.forName("com.pinterest.ktlint.core.api.DefaultEditorConfigProperties") + val defaultEditorConfigProperties = defaultEditorConfigPropertiesClass.kotlin.objectInstance + val codeStyle = getCodeStyle(if (userData["android"]?.toBoolean() == true) "android" else "official") + val editorConfigOverrideClass = Class.forName("com.pinterest.ktlint.core.api.EditorConfigOverride") + val editorConfigOverride = editorConfigOverrideClass.kotlin.primaryConstructor!!.call() + val addMethod = editorConfigOverrideClass.getDeclaredMethod("add", getEditorConfigPropertyClass(), Any::class.java) + addMethod.isAccessible = true + val disabledRulesProperty = defaultEditorConfigPropertiesClass.kotlin.memberProperties.firstOrNull { it.name == "ktlintDisabledRulesProperty" } + ?: defaultEditorConfigPropertiesClass.kotlin.memberProperties.first { it.name == "disabledRulesProperty" } + val codeStyleSetProperty = defaultEditorConfigPropertiesClass.kotlin.memberProperties.first { it.name == "codeStyleSetProperty" } + addMethod.invoke(editorConfigOverride, disabledRulesProperty.getter.call(defaultEditorConfigProperties), userData["disabled_rules"]) + addMethod.invoke(editorConfigOverride, codeStyleSetProperty.getter.call(defaultEditorConfigProperties), codeStyle) + return editorConfigOverride +} + +@OptIn(FeatureInAlphaState::class) +internal class ExperimentalParamsProviderInvocation: KtLintInvocation { + private var editorConfigPath: String? = null + private var debug: Boolean = false + private lateinit var ruleProviders: Set + private lateinit var userData: Map + fun initialize( + editorConfigPath: String?, + ruleProviders: Set, + userData: Map, + debug: Boolean + ) { + this.editorConfigPath = editorConfigPath + this.ruleProviders = ruleProviders + this.userData = userData + this.debug = debug + } + private fun buildParams(file: File, cb: (LintError, Boolean) -> Unit): com.pinterest.ktlint.core.KtLint.ExperimentalParams { + val script = !file.name.endsWith(".kt", ignoreCase = true) + val ctor = Class.forName("com.pinterest.ktlint.core.KtLint\$ExperimentalParams").kotlin.primaryConstructor + val editorConfigOverride = userDataToEditorConfigOverride(userData, true) + return ctor!!.callBy( + mapOf( + ctor.findParameterByName("fileName")!! to file.absolutePath, + ctor.findParameterByName("text")!! to file.readText(), + ctor.findParameterByName("ruleProviders")!! to ruleProviders, + ctor.findParameterByName("cb")!! to cb, + ctor.findParameterByName("script")!! to script, + ctor.findParameterByName("editorConfigPath")!! to editorConfigPath, + ctor.findParameterByName("debug")!! to debug, + ctor.findParameterByName("editorConfigOverride")!! to editorConfigOverride + ) + ) as com.pinterest.ktlint.core.KtLint.ExperimentalParams + } + + override fun invokeLint(file: File, cb: (LintError, Boolean) -> Unit) { + com.pinterest.ktlint.core.KtLint.lint(buildParams(file, cb)) + } + + override fun invokeFormat(file: File, cb: (LintError, Boolean) -> Unit): String { + return com.pinterest.ktlint.core.KtLint.format(buildParams(file, cb)) + } +} + +internal class RuleEngineInvocation : KtLintInvocation { + private lateinit var engine: Any + fun initialize(ruleProviders: Set, userData: Map) { + val engineClass = Class.forName("com.pinterest.ktlint.core.KtLintRuleEngine") + val editorConfigOverride = userDataToEditorConfigOverride(userData, true) + val ctor = engineClass.kotlin.primaryConstructor + engine = ctor!!.callBy( + mapOf( + ctor.findParameterByName("ruleProviders")!! to ruleProviders, + ctor.findParameterByName("editorConfigOverride")!! to editorConfigOverride + ) + ) + } + + override fun invokeLint(file: File, cb: (LintError, Boolean) -> Unit) { + engine::class.declaredMemberFunctions.forEach { println(it.name + " " + it.parameters.map { it.name }.joinToString(",")) } + val lintMethod = engine::class.memberFunctions.first { + it.name == "lint" + && it.parameters.map { it.name }.containsAll(setOf("code", "filePath", "callback")) + } + lintMethod.callBy( + mapOf( + lintMethod.instanceParameter!! to engine, + lintMethod.findParameterByName("code")!! to file.readText(), + lintMethod.findParameterByName("filePath")!! to file.absoluteFile.toPath(), + lintMethod.findParameterByName("callback")!! to { le: LintError -> cb.invoke(le, false) } + ) + ) + } + + override fun invokeFormat(file: File, cb: (LintError, Boolean) -> Unit): String { + val formatMethod = engine::class.memberFunctions.first { + it.name == "format" + && it.parameters.map { it.name }.containsAll(setOf("code", "filePath", "callback")) + } + return formatMethod.callBy( + mapOf( + formatMethod.instanceParameter!! to engine, + formatMethod.findParameterByName("code")!! to file.readText(), + formatMethod.findParameterByName("filePath")!! to file.absoluteFile.toPath(), + formatMethod.findParameterByName("callback")!! to cb + ) + ) as String + } +} + +internal fun selectInvocation(): KtLintInvocation { + return try { + KtLintInvocation::class.java.classLoader.loadClass("com.pinterest.ktlint.core.KtLint\$Params") + LegacyParamsInvocation() + } catch (e: Exception) { + try { + Class.forName("com.pinterest.ktlint.core.KtLintRuleEngine") + RuleEngineInvocation() + } catch (e: Exception) { + val ctor = Class.forName("com.pinterest.ktlint.core.KtLint\$ExperimentalParams").kotlin.primaryConstructor + if (ctor?.findParameterByName("ruleProviders") != null) { + ExperimentalParamsProviderInvocation() + } else { + ExperimentalParamsInvocation() + } + } + } +} diff --git a/plugin/src/main/kotlin/org/jlleitschuh/gradle/ktlint/worker/KtLintRuleLoader.kt b/plugin/src/main/kotlin/org/jlleitschuh/gradle/ktlint/worker/KtLintRuleLoader.kt new file mode 100644 index 00000000..9d11e71d --- /dev/null +++ b/plugin/src/main/kotlin/org/jlleitschuh/gradle/ktlint/worker/KtLintRuleLoader.kt @@ -0,0 +1,30 @@ +package org.jlleitschuh.gradle.ktlint.worker + +import java.util.ServiceLoader +import kotlin.reflect.full.memberProperties + +internal fun loadRuleSetsFromClasspathWithRuleSetProvider(): Map { + return ServiceLoader + .load(com.pinterest.ktlint.core.RuleSetProvider::class.java) + .associateBy { + val key = it.get().id + // Adapted from KtLint CLI module + if (key == "standard") "\u0000$key" else key + } + .mapValues { it.value.get() } +} + +internal fun loadRuleSetsFromClasspathWithRuleSetProviderV2(): Map> { + val ruleSetProviderV2Class = Class.forName("com.pinterest.ktlint.core.RuleSetProviderV2") + val idProperty = ruleSetProviderV2Class.kotlin.memberProperties.first{it.name == "id"} + val getRuleProviders = ruleSetProviderV2Class.getDeclaredMethod("getRuleProviders") + return ServiceLoader + .load(ruleSetProviderV2Class) + .associateBy { + val key = idProperty.getter.call(it) as String + // Adapted from KtLint CLI module + if (key == "standard") "\u0000$key" else key + }.mapValues { + getRuleProviders.invoke(it.value) as Set + } +} diff --git a/plugin/src/main/kotlin/org/jlleitschuh/gradle/ktlint/worker/KtLintWorkAction.kt b/plugin/src/main/kotlin/org/jlleitschuh/gradle/ktlint/worker/KtLintWorkAction.kt index 76728d26..256303cf 100644 --- a/plugin/src/main/kotlin/org/jlleitschuh/gradle/ktlint/worker/KtLintWorkAction.kt +++ b/plugin/src/main/kotlin/org/jlleitschuh/gradle/ktlint/worker/KtLintWorkAction.kt @@ -2,9 +2,6 @@ package org.jlleitschuh.gradle.ktlint.worker import com.pinterest.ktlint.core.KtLint import com.pinterest.ktlint.core.LintError -import com.pinterest.ktlint.core.ParseException -import com.pinterest.ktlint.core.RuleSet -import com.pinterest.ktlint.core.RuleSetProvider import net.swiftzer.semver.SemVer import org.apache.commons.io.input.MessageDigestCalculatingInputStream import org.gradle.api.GradleException @@ -20,7 +17,6 @@ import java.io.File import java.io.ObjectInputStream import java.io.ObjectOutputStream import java.io.Serializable -import java.util.ServiceLoader @Suppress("UnstableApiUsage") abstract class KtLintWorkAction : WorkAction { @@ -28,11 +24,6 @@ abstract class KtLintWorkAction : WorkAction() val formattedFiles = mutableMapOf() + val ktlintInvoker = selectInvocation() + when (ktlintInvoker) { + is LegacyParamsInvocation -> { + ktlintInvoker.initialize( + editorConfigPath = additionalEditorConfig, + ruleSets = loadRuleSetsFromClasspathWithRuleSetProvider().filterRules( + parameters.enableExperimental.getOrElse(false), + parameters.disabledRules.getOrElse(emptySet()) + ), + userData = userData, + debug = debug + ) + } + + is ExperimentalParamsInvocation -> { + ktlintInvoker.initialize( + editorConfigPath = additionalEditorConfig, + ruleSets = loadRuleSetsFromClasspathWithRuleSetProvider().filterRules( + parameters.enableExperimental.getOrElse(false), + parameters.disabledRules.getOrElse(emptySet()) + ), + userData = userData, + debug = debug + ) + } + + is ExperimentalParamsProviderInvocation -> { + ktlintInvoker.initialize( + editorConfigPath = additionalEditorConfig, + ruleProviders = loadRuleSetsFromClasspathWithRuleSetProviderV2().filterRules( + parameters.enableExperimental.getOrElse(false), + parameters.disabledRules.getOrElse(emptySet()) + ).flatten().toSet(), + userData = userData, + debug = debug + ) + } + + is RuleEngineInvocation -> { + ktlintInvoker.initialize( + loadRuleSetsFromClasspathWithRuleSetProviderV2().filterRules( + parameters.enableExperimental.getOrElse(false), + parameters.disabledRules.getOrElse(emptySet()) + ).flatten().toSet(), userData + ) + } + } parameters.filesToLint.files.forEach { val errors = mutableListOf>() - val ktLintParameters = KtLint.Params( - fileName = it.absolutePath, - text = it.readText(), - ruleSets = ruleSets, - userData = userData, - debug = debug, - editorConfigPath = additionalEditorConfig, - script = !it.name.endsWith(".kt", ignoreCase = true), - cb = { lintError, isCorrected -> - errors.add(lintError to isCorrected) - } - ) try { if (formatSource) { val currentFileContent = it.readText() - val updatedFileContent = KtLint.format(ktLintParameters) + val updatedFileContent = ktlintInvoker.invokeFormat(it) { lintError, isCorrected -> + errors.add(lintError to isCorrected) + } if (updatedFileContent != currentFileContent) { formattedFiles[it] = contentHash(it) it.writeText(updatedFileContent) } } else { - KtLint.lint(ktLintParameters) + ktlintInvoker.invokeLint(it) { lintError, isCorrected -> + errors.add(lintError to isCorrected) + } } - } catch (e: ParseException) { + } catch (e: RuntimeException) { throw GradleException( "KtLint failed to parse file: ${it.absolutePath}", e @@ -126,24 +156,11 @@ abstract class KtLintWorkAction : WorkAction - ): Set = loadRuleSetsFromClasspath() - .filterKeys { enableExperimental || it != "experimental" } - .filterKeys { !(disabledRules.contains("standard") && it == "\u0000standard") } - .toSortedMap() - .mapValues { it.value.get() } - .values - .toSet() - - private fun loadRuleSetsFromClasspath(): Map = ServiceLoader - .load(RuleSetProvider::class.java) - .associateBy { - val key = it.get().id - // Adapted from KtLint CLI module - if (key == "standard") "\u0000$key" else key - } + private fun Map.filterRules(enableExperimental: Boolean, disabledRules: Set): Set { + return this.filterKeys { enableExperimental || it != "experimental" } + .filterKeys { !(disabledRules.contains("standard") && it == "\u0000standard") } + .toSortedMap().mapValues { it.value }.values.toSet() + } interface KtLintWorkParameters : WorkParameters { val filesToLint: ConfigurableFileCollection 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 f1b870a9..fecf38ff 100644 --- a/plugin/src/test/kotlin/org/jlleitschuh/gradle/ktlint/KtLintSupportedVersionsTest.kt +++ b/plugin/src/test/kotlin/org/jlleitschuh/gradle/ktlint/KtLintSupportedVersionsTest.kt @@ -112,6 +112,11 @@ class KtLintSupportedVersionsTest : AbstractPluginTest() { // "0.43.0" does not work on JDK1.8 // "0.43.1" asked not to use it "0.43.2", + "0.44.0", + "0.45.2", + "0.46.1", + "0.47.1", + "0.48.0" ).also { // "0.37.0" is failing on Windows machines that is fixed in the next version if (!OS.WINDOWS.isCurrentOs) it.add("0.37.0") 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 61bb4f6c..805e70ef 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 @@ -100,8 +100,8 @@ class TestProject( } companion object { - const val CLEAN_SOURCES_FILE = "src/main/kotlin/clean-source.kt" - const val FAIL_SOURCE_FILE = "src/main/kotlin/fail-source.kt" + const val CLEAN_SOURCES_FILE = "src/main/kotlin/CleanSource.kt" + const val FAIL_SOURCE_FILE = "src/main/kotlin/FailSource.kt" } }