ktrs 0.4.0

Fast Kotlin formatter and linter: ktfmt-identical formatting and ktlint-identical linting, with drop-in `ktfmt` and `ktlint` binaries
ktrs-0.4.0 is not a library.

ktrs

Kotlin formatting and linting in Rust, without starting a JVM.

CI crates.io License Playground

ktrs is a native replacement for ktfmt and ktlint. It produces the same output, it is 13-190x faster, and it ships as small native binaries with no runtime.

  • ⚡ Fast. Under 10 ms per file in an editor or pre-commit hook, against roughly a second of JVM startup. On whole projects it uses every core and is still at least 13x faster.
  • 🎯 Identical output. Byte-identical to ktfmt 0.64 on 6,121 of 6,123 real-world files (both tools reject the other two). Lint violations and --format output match ktlint 2.0 in all three code styles, and ktlint 1.8 in a compatibility mode.
  • 🔌 Drop-in. The ktfmt and ktlint binaries accept the originals' flags, messages and exit codes, so existing scripts, hooks and CI keep working.
  • 🧩 Fits your setup. Integrations for GitHub Actions, pre-commit, Spotless, ktfmt-gradle and ktlint-gradle drop-in plugins, and Neovim, Helix, Zed, Emacs and VS Code.
  • 🌳 Built on a faithful parser. ktrs includes a lossless Kotlin parser whose tree matches the Kotlin compiler's PSI node for node.

Try the formatter in your browser in the playground, which runs it as WebAssembly.

Installation

curl -fsSL https://raw.githubusercontent.com/Hexay/ktrs/master/install.sh | sh   # prebuilt binaries
cargo install ktrs                                                                # from crates.io
brew tap hexay/ktrs https://github.com/Hexay/ktrs && brew install hexay/ktrs/ktrs  # Homebrew

One install puts three binaries on your PATH: ktrs, ktfmt and ktlint. The install script also works on Windows under Git Bash; otherwise, download a zip from Releases.

Usage

ktrs fmt                              # format every .kt/.kts under the current directory
ktrs fmt --style kotlinlang src/      # styles: meta (default), google, kotlinlang
ktrs fmt --check                      # CI: list files that would change, exit 1 if any
ktrs fmt - < Foo.kt                   # stdin to stdout, for editors

ktrs lint                             # check every .kt/.kts under the current directory
ktrs lint --format src/               # autocorrect what can be fixed, report the rest
ktrs lint --reporter json - < Foo.kt  # reporters: plain, json, checkstyle, sarif, html, ...

Drop-in for ktfmt and ktlint

The ktfmt and ktlint binaries accept the original command lines exactly, so anything that runs the jars can run them instead:

ktfmt --kotlinlang-style --set-exit-if-changed src/
ktlint --relative "src/**/*.kt" "!src/**/generated/**"
  • ktfmt supports all of ktfmt's flags, plus @argfile, - for stdin and --enable-editorconfig.
  • ktlint supports ! negation in patterns, -F, --stdin, --patterns-from-stdin, --baseline, --editorconfig, every built-in reporter, and the git hook subcommands.
  • ktlint implements 2.0.0-ALPHA-4, with its engine and all 105 standard rules.

ktlint 1.8. Most teams still run ktlint 1.x, and 2.0 changed rule results, autocorrect order, exit codes and baseline matching (research/22). To get 1.8.0 behaviour, add this to .editorconfig (the ktlint jars ignore it), or pass --ktlint-version=1.8:

[*.{kt,kts}]
ktrs_ktlint_version = 1.8

This mode ports 1.8's rule differences, its rule-by-rule autocorrect order and its CLI. On 21,000 files it matches the 1.8.0 jar except for a few KDoc whitespace rows, where 1.8's older Kotlin lexer splits trailing spaces differently (research/26).

Custom rule sets. -R jars are supported, in ktlint and ktrs lint. compose-rules 0.6.7, by far the most used rule set, runs natively with output identical to the jar (the -all.jar and the Maven artifacts). Any other rule set or reporter jar hands the whole run to the real ktlint jar (downloaded once and checked by SHA-256; needs Java), so it works at JVM speed (research/27).

Limits.

  • kotlinter and the Maven plugins run ktlint inside the JVM and don't use these binaries.
  • The ktfmt binary accepts Kotlin 2.4 syntax (e.g. companion { } blocks) that ktfmt 0.64, built on Kotlin 2.3, rejects.

Integrations

GitHub Actions

- uses: Hexay/ktrs@v0.4.0          # Linux, macOS and Windows
- run: ktrs fmt --check --style kotlinlang
- run: ktrs lint

pre-commit

No Rust needed: on first run, the hook downloads the release binaries for its rev.

- repo: https://github.com/Hexay/ktrs
  rev: v0.4.0
  hooks:
    - id: ktrs-fmt          # also: ktrs-fmt-check, ktfmt (with ktfmt's flags in `args`)
      args: [--style, kotlinlang]
    - id: ktrs-lint         # also: ktrs-lint-format, ktlint (with ktlint's flags in `args`)

Editors

Editors pass the buffer on stdin. --stdin-name gives ktrs the file's path so .editorconfig applies. Add --style google or --style kotlinlang if you need them.

Neovim (conform.nvim):

formatters_by_ft = { kotlin = { "ktrs" } },
formatters = { ktrs = { command = "ktrs", args = { "fmt", "--editorconfig", "--stdin-name", "$FILENAME", "-" } } },

Helix (languages.toml):

[[language]]
name = "kotlin"
formatter = { command = "ktrs", args = ["fmt", "-"] }
auto-format = true

Zed (settings.json):

"languages": { "Kotlin": { "formatter": { "external": {
  "command": "ktrs", "arguments": ["fmt", "--editorconfig", "--stdin-name", "{buffer_path}", "-"] } } } }

Emacs (apheleia):

(push '(ktrs . ("ktrs" "fmt" "--editorconfig" "--stdin-name" filepath "-")) apheleia-formatters)
(setf (alist-get 'kotlin-mode apheleia-mode-alist) 'ktrs)

VS Code (Custom Local Formatters):

"customLocalFormatters.formatters": [
  { "command": "ktrs fmt --editorconfig --stdin-name \"${file}\" -", "languages": ["kotlin"] } ]

Gradle

ktfmt-gradle drop-in. The io.github.hexay.ktrs plugin replaces ktfmt-gradle 0.27.0. Change only the plugin id. The ktfmt { } block, the ktfmtCheck/ktfmtFormat* tasks, --include-only and the com.ncorti.ktfmt.gradle.* imports keep working.

plugins {
    id("io.github.hexay.ktrs") version "0.4.0"   // was: id("com.ncorti.ktfmt.gradle") version "0.27.0"
}

ktlint-gradle drop-in. The io.github.hexay.ktrs.ktlint plugin replaces ktlint-gradle 14.2.0 the same way: the ktlint { } block, ktlintCheck/ktlintFormat and the per-source-set, baseline and git hook tasks, ktlintRuleset(...) and the org.jlleitschuh.gradle.ktlint.* types keep working. Console output, reports and formatted files match the original with ktlint 1.8.0 (research/29).

plugins {
    id("io.github.hexay.ktrs.ktlint") version "0.4.0"   // was: id("org.jlleitschuh.gradle.ktlint") version "14.2.0"
}

version defaults to "1.8.0"; "2.0.0-ALPHA-4" selects 2.0, and other versions fail the build. compose-rules runs natively; other rule sets run the task through the real ktlint jar.

Spotless. KtrsStep replaces ktfmt() and KtrsKtlintStep replaces ktlint() (Spotless 7+):

buildscript {
    repositories { maven("https://hexay.github.io/ktrs/maven") }
    dependencies { classpath("io.github.hexay:ktrs:0.4.0") }
}

spotless {
    kotlin {
        addStep(io.github.hexay.ktrs.spotless.KtrsStep.create(io.github.hexay.ktrs.KtrsOptions.kotlinlang()))
        // or, instead of ktlint("1.8.0").editorConfigOverride(...).customRuleSets(...):
        addStep(io.github.hexay.ktrs.spotless.KtrsKtlintStep.create(io.github.hexay.ktrs.KtlintOptions.defaults()
            .withEditorConfigPath(rootProject.file(".editorconfig"))
            .withEditorConfigOverride(mapOf("ktlint_code_style" to "ktlint_official"))))
    }
}

KtrsKtlintStep gives the same results as Spotless's ktlint("1.8.0") step (research/28). withCustomRuleSets(files) takes jar files; only compose-rules is supported there, other rule sets need Spotless's ktlint().

Until the plugin is on the Gradle Plugin Portal, add the repository in settings.gradle.kts:

pluginManagement {
    repositories {
        gradlePluginPortal()
        maven("https://hexay.github.io/ktrs/maven")
    }
}

The io.github.hexay:ktrs jar has no dependencies. It bundles the native binaries for Linux, macOS and Windows (x86-64 and ARM) and keeps long-lived ktrs serve processes, so a build starts the binary once, not once per file.

  • KtrsOptions mirrors ktfmt's options: start from meta(), google() or kotlinlang(), then chain withMaxWidth, withBlockIndent, withContinuationIndent, withRemoveUnusedImports, withTrailingCommas and withEditorConfig(true).
  • From other JVM code, Ktrs.create() returns a thread-safe formatter: ktrs.format(code, KtrsOptions.google()), or ktrs.ktlint(code, KtlintOptions.defaults(), path) for ktlint's formatted code and remaining violations.
  • The plugin accepts useClassloaderIsolation, processIsolationJvmArgs and ktfmtClasspath but ignores them. debuggingPrintOpsAfterFormatting only logs a warning.
  • To use a different binary from the bundled one, set the Gradle property ktrs.executable.
  • Without the jar, Spotless's generic step runs the binary once per file: nativeCmd("ktfmt", "/path/to/ktfmt", listOf("--kotlinlang-style", "-")).

Performance

Each tool is run from the command line the way users run it: same flags, same files, identical output. Timings are wall time, the median of 5 runs after a warm-up, measured with hyperfine on Linux (Xeon E-2136, 10 CPUs, JDK 21).

Formatting, the ktfmt binary against the ktfmt 0.64 jar:

Scenario ktrs ktfmt 0.64 Speedup
Editor: one 8 KB file on stdin <5 ms 791 ms >150x
Pre-commit: 10 changed files 16 ms 743 ms 47x
CI check on okhttp (617 files) 183 ms 3.87 s 21x
Format okhttp in place 176 ms 3.30 s 19x
Format okhttp in place, 1 core 505 ms 12.46 s 25x
Format 7 projects (6,123 files, 31 MB) 813 ms 10.91 s 13x

Linting, the ktlint binary against the ktlint 2.0.0-ALPHA-4 jar:

Scenario ktrs ktlint 2.0 Speedup
Editor: one 8 KB file on stdin <10 ms 1.06 s >100x
Lint one file <10 ms 845 ms >80x
Lint okhttp (617 files) 344 ms 11.56 s 34x
Autocorrect okhttp (-F) 640 ms 123.0 s 192x
Lint okhttp, 1 core 1.37 s · 49 MB 39.36 s · 366 MB 29x
Lint 7 projects (6,123 files) 1.72 s · 268 MB 49.73 s · 553 MB 29x
Autocorrect 7 projects (-F) 4.00 s 338.9 s 85x

The ktlint 1.8.0 jar is 10-30% faster than 2.0 on these runs. ktrs's 1.8 mode visits the tree once per rule, as 1.8 does, and is up to 2x slower than its 2.0 mode.

JVM startup dominates small runs. On large runs ktrs is still several times faster per core, and it uses every core. The binary is a few MB with no runtime, compared with a 71 MB jar plus a JRE.

  • Corpus. The 7 projects are okhttp, kotlinx.coroutines, nowinandroid, ktlint, ktfmt, Exposed and ktor, pinned in tools/bench/REVISIONS.
  • 1 core. Both processes are pinned to one CPU from launch. The JVM then sizes its GC and JIT threads for one CPU, as it would in a 1-CPU container, and its JIT competes with the work.
  • Identical output. ktfmt rejects 2 of the 6,123 files, Exposed's {{packageName}} code-generator templates, and ktrs rejects them with the same error.
  • Reproduce. tools/bench/public.sh (Linux or macOS; needs hyperfine, Java and a cargo build --profile dist) fetches the corpus, downloads the jars and checks their SHA-256, and runs every scenario above; --only quick runs a subset. The bench workflow runs it on a GitHub runner for each release tag or on demand. Method, noise and the ktlint 1.8 figures are in research/23.

Spotless. spotlessApply on okhttp's 573 files gives identical output. The figures are the formatter's share, after subtracting a Spotless run that only trims whitespace:

Spotless step ktrs (KtrsStep) ktfmt 0.64 (ktfmt())
Fresh Gradle daemon, as in CI ~4 s ~52 s
Warm daemon, repeated runs ~2 s ~6 s

How correctness is checked

Parity with the original tools is the spec. The ported test suites run in CI, and the corpus diffs (cargo corpus-diff, cargo fmt-diff, cargo lint-diff) compare against the real tools on ~6,000 files:

  • Parser. The tree must match the Kotlin compiler's PSI (DebugUtil.psiToString) on the compiler's own test fixtures and on every corpus file.

  • Formatter. ktfmt's test suite is ported. The output is also diffed byte for byte against the ktfmt jar on the corpus in the meta, google and kotlinlang styles.

  • Linter. ktlint's rule tests are ported. Violations and --format output are diffed against the ktlint jar on the corpus in the ktlint_official, intellij_idea and android_studio code styles, with and without experimental rules.

  • CLIs. The ktfmt and ktlint binaries are compared with the jars on stdout, stderr, exit code and written files across a scenario suite.

  • Held-out corpus. To check that the corpus work didn't overfit, the CLIs also run against a second corpus that never drove a fix. It has 15,287 files from 20 other projects, pinned in tools/holdout/REVISIONS. In the first run:

    • All 2.2M lint violations matched ktlint in all three styles.
    • ktfmt output matched on every file in all three styles.
    • --format output differed on 3 files, from one rule. That bug is now fixed.

    Details are in research/21; tools/holdout/run.sh reruns it. The ktlint 1.8 mode and compose-rules are checked against their jars on both corpora too.

  • Fuzzing. fuzz/ has cargo-fuzz targets for the parser, ktfmt and ktlint, and tools/fuzz/diff.sh runs mutated real-world files through the binaries and the jars and reports any difference. The first runs found 4 bugs, now fixed, including exponential parser memory on deeply nested generic-looking input (research/24).

  • Upstream releases. A weekly workflow opens an issue when ktfmt, ktlint, compose-rules or Kotlin publishes a release newer than the pinned version.

Maintenance

ktrs is maintained by @Hexay. It tracks ktfmt 0.64, ktlint 2.0.0-ALPHA-4 (plus 1.8.0 in compatibility mode), compose-rules 0.6.7 and the Kotlin 2.4.20 parser. New upstream releases are ported and re-checked against the parity gates before a ktrs release. If ktrs output ever differs from ktfmt or ktlint on your code, that's a bug: please open an issue with the file, or a snippet that reproduces it, and the command line you used.

Contributing

tools/sync-kotlin.sh                  # pinned upstream sources + vendored test fixtures
tools/psi-dump/psi-dump.sh one X.kt   # reference PSI tree from the real compiler (needs a JDK)
cargo xtask codegen                   # regenerate SyntaxKind from crates/ktrs-syntax/kinds.tsv
cargo test

CLAUDE.md lists the crate layout and every parity gate. Design notes are in research/.

License

Dual-licensed under MIT or Apache-2.0, at your option. ktrs contains code and test data ported from the Kotlin compiler, the IntelliJ Platform, ktfmt, google-java-format, ec4j (Apache-2.0), ktlint and ktfmt-gradle (MIT). See NOTICE.