ktlint のフォーマットをプログラム上で実行したい
ktlint のフォーマット処理をプログラム上で呼びたいなと思いました.
Go で言うところの
import "go/format"
src := goSourceCode() // []byte だか string だか
format.Source(src)
的なことです. つまり,
val src = kotlinSourceCode()
val formatted = Ktlint.format(src)
みたいなことがしたい.
前提
ktlint v0.39.0
ひとまず調査
どこかに format: (String) -> String
的な何かがあるのだろうと思いながら潜ります.
エントリーポイントとなる gradle のタスク定義から始めていきます.
task ktlint(type: JavaExec, group: LifecycleBasePlugin.VERIFICATION_GROUP) {
description = "Check Kotlin code style."
classpath = configurations.ktlint
main = 'com.pinterest.ktlint.Main'
args '*/src/**/*.kt'
}
https://github.com/pinterest/ktlint/blob/0.39.0/build.gradle#L41
タスク定義で指定されているメイン関数(のあるクラス定義).
fun main(args: Array<String>) {
val ktlintCommand = KtlintCommandLine()
// ...
format
オプションが指定されたときの処理を追いかけたいので, オプションの定義を探します.
@Option(
names = ["--format", "-F"],
description = ["Fix any deviations from the code style"]
)
private var format: Boolean = false
format
オプションが指定されている場合の処理を探します.
if (format) {
val formattedFileContent = try {
formatFile(
fileName,
fileContent,
ruleSetProviders.map { it.value.get() },
userData,
editorConfigPath,
debug
) { err, corrected ->
// ...
ktlint
コマンドの実装を抜けて, com.pinterest.ktlint.core
パッケージの関数呼び出しまでたどり着きました.
internal fun formatFile(
fileName: String,
fileContents: String,
ruleSets: Iterable<RuleSet>,
userData: Map<String, String>,
editorConfigPath: String?,
debug: Boolean,
cb: (e: LintError, corrected: Boolean) -> Unit
): String =
KtLint.format(
KtLint.Params(
fileName = fileName,
text = fileContents,
ruleSets = ruleSets,
userData = userData,
script = !fileName.endsWith(".kt", ignoreCase = true),
editorConfigPath = editorConfigPath,
cb = cb,
debug = debug
)
)
結局, 以下の Ktlint.format
を呼び出せば良さそうです.
public fun format(params: Params): String {
// ...
もうちょっと調査
実際にこの KtLint.format
を呼び出してみます.
引数の Params
が何者なのか確認します.
/**
* @param fileName path of file to lint/format
* @param text Contents of file to lint/format
* @param ruleSets a collection of "RuleSet"s used to validate source
* @param userData Map of user options
* @param cb callback invoked for each lint error
* @param script true if this is a Kotlin script file
* @param editorConfigPath optional path of the .editorconfig file (otherwise will use working directory)
* @param debug True if invoked with the --debug flag
*/
public data class Params(
val fileName: String? = null,
val text: String,
val ruleSets: Iterable<RuleSet>,
val userData: Map<String, String> = emptyMap(),
val cb: (e: LintError, corrected: Boolean) -> Unit,
val script: Boolean = false,
val editorConfigPath: String? = null,
val debug: Boolean = false
) {
ruleSets
を適切に設定さえできればひとまず動かせそうに見えます.
ひとまず, formatFile
の呼び出し元を戻ってみます. KtlintCommandLine.run
まで戻ってきました.
fun run() {
failOnOldRulesetProviderUsage()
val start = System.currentTimeMillis()
val ruleSetProviders = rulesets.loadRulesets(experimental, debug)
val reporter = loadReporter()
val userData = listOfNotNull(
"android" to android.toString(),
if (disabledRules.isNotBlank()) "disabled_rules" to disabledRules else null
).toMap()
reporter.beforeAll()
if (stdin) {
lintStdin(ruleSetProviders, userData, reporter)
} else {
lintFiles(ruleSetProviders, userData, reporter)
}
// ...
loadRulesets
をみてみると, ServiceLoader
を利用して RuleSetProvider
をロードしていることがわかります.
private fun loadRulesets(externalRulesetsJarPaths: List<String>) = ServiceLoader
.load(
RuleSetProvider::class.java,
URLClassLoader(externalRulesetsJarPaths.toFilesURIList().toTypedArray())
)
// ...
RuleSetProvider
の実態はどこで定義されているんだと探してみると, ktlint-ruleset-standard
プロジェクト配下にいます.
class StandardRuleSetProvider : RuleSetProvider {
// ...
実行してみる
この RuleSetProvider
を利用すればひとまず最低限のフォーマットが動作するのではないかと期待して試しに実行してみます.
package com.example
import com.pinterest.ktlint.core.KtLint
import com.pinterest.ktlint.ruleset.standard.StandardRuleSetProvider
fun main() {
val code = """
class Foo(
val id : String, val age : Int,
val name:String )
{
fun foo() : Boolean { return name.isEmpty()
}
}
""".trimIndent()
val ruleSets = setOf(
StandardRuleSetProvider().get()
)
val formatted = KtLint.format(
KtLint.Params(
fileName = null,
text = code,
ruleSets = ruleSets,
userData = emptyMap(),
cb = { _, _ -> run {} },
script = false,
editorConfigPath = null,
debug = false
)
)
println(formatted)
}
// Output:
//
// class Foo(
// val id: String,
// val age: Int,
// val name: String
// ) {
// fun foo(): Boolean {
// return name.isEmpty()
// }
// }
動きました. めでたしめでたし.
まとめ
とりあえず ktlint
をプログラム上で実行してコードをフォーマットすることはできた.
サンプルコード全体: https://github.com/lusingander/ktlint-format-example