Built on JetBrains’ own grammar, for codebases that are half Java.
Kotlin is one of very few languages whose vendor publishes its grammar as an ANTLR file. We use that file. What it buys you is a settled definition of what is legal — and what it does not buy you is on this page too.
This is a younger engine than our COBOL or SAS ones and we are going to say so. Kotlin through 1.9, no scripts, no symbol resolution. The case for it is the Java boundary, not breadth.
KotlinParser.g4 · KotlinLexer.g4 · UnicodeClasses.g4 · published by JetBrains
Most of the catalog exists because a language has no published grammar and somebody has to reverse-engineer it from a compiler’s behavior. Kotlin is the opposite case, and it changes what you are buying.
JetBrains maintains a formal Kotlin specification, and the grammar in it is published as an ANTLR file — the reference grammar page on kotlinlang.org is generated from it. That means the definition of what is legal Kotlin is a versioned artifact you can diff between releases, not an inference from running the compiler and watching what happens.
We use that file. Practically, it removes an entire class of risk that dominates COBOL, RPG and VB6 work: nobody has to argue about whether a construct is really in the language, because there is a document that says so.
What it does not remove is the hard part. The official grammar is written for a specification, not for a production parser — the newline handling alone, with optional-newline markers threaded through almost every rule, makes it awkward to build on. And Kotlin resolves several genuine ambiguities with semantic rules the grammar cannot express.
Our additions to it are unglamorous and worth naming anyway: a cache-reset hook and a bounded parser cache cycle, so that parsing a large codebase in one process does not accumulate ANTLR decision state until memory runs out. That is the kind of change that only shows up after someone runs the thing over a real repository.
Input — Kotlin
Report.kt
@file:JvmName("Reports")package com.example.report
sealed interface Result<out T>
data class Ok<T>(val value: T) : Result<T>
infix fun String?.orElse(f: String): String = this ?: fclass Report(private val rows: List<Row>) {
val summary: String by lazy { render() }private fun render(): String {
val active = rows.filter { it.qty > 0 }
return"rows: ${active.size} of ${rows.size}"
}
fun `total across all rows`(): Int = rows.sumOf { it.qty }
}
Four constructs with no Java equivalent in twelve lines: a file-level annotation that renames the Java-visible class, an infix extension on a nullable receiver, a delegated property, and an identifier containing spaces.
Real rule names from the JetBrains grammar. The AST you program against is derived from this tree — 95 K-prefixed node classes with a position on each — and the derivation is not yet complete for the whole grammar, which is stated plainly in the limits below.
What it covers, and what it does not
Four rows in this table are negative. That is deliberate: on a language whose vendor gives away a better analysis toolchain than any third party can build, the useful thing a vendor page can do is be precise.
Checked against the grammar, the AST and the module configuration we ship, not against the language reference.
Supported — the interpolated code is parsed as expressions
Comments
hidden channel, including inside interpolations
Retained
data and inline
carried as modifiers on the declaration
Parsed, but with no dedicated AST concept of their own
AST coverage of the full grammar
the parse-tree-to-AST transformer
Incomplete — the ANTLR layer parses more than the AST layer maps
Kotlin scripts, .kts
the script rule exists but is never invoked
Not supported — Gradle build files will not parse
Kotlin 2.x syntax additions
—
Not in the grammar
Symbol resolution, type inference, call graphs
—
Not implemented — imports and syntactic references only
Platform types, T!
—
Invisible — they have no source syntax, so no parser can surface them
Three of those rows deserve a sentence each. Scripts:.kt and .kts are different start rules producing structurally different trees, and only the first is wired up — so a tool that reads a Kotlin project cannot read its build.gradle.kts. The rule is already in the grammar, so enabling it is scoped work rather than research; ask. Version: Kotlin 2.0 was a compiler-frontend rewrite rather than a syntactic overhaul, but 2.x has added syntax, and none of it is here. Symbol resolution: there is none, and that bounds what a dependency analysis on this engine can honestly claim — you get imports and syntactic references, not resolved ones.
Four constructs that decide whether a Kotlin parser is real
String templates nest without bound
A ${…} field holds a full expression, which may hold a lambda, which may hold another string, which may hold another field. The lexer has to count braces and switch modes indefinitely — and correctly allow comments and newlines inside an interpolation. This is the strongest single thing the official grammar gives us.
There is no statement terminator. A newline ends a statement unless the expression is incomplete, or the next line starts with an infix operator, or you are inside brackets, or a lambda follows. This is why the official grammar is threaded with optional-newline markers, and it is a genuine source of bugs in client code.
fun f() = 1 + 2// not 3: a stray unary +2
Almost everything is a soft keyword
by, where, get, set, field, it, value, data, inner, sealed, expect, actual, constructor and init are all usable as identifiers. And backtick identifiers may contain spaces, which is normal in test code rather than exotic.
val data = 1; val sealed = 2fun `it should fail`() {}
Nullability is syntax; smart casts are not
String and String? are grammar. String! — the platform type Kotlin assigns to an unannotated Java value — is deliberately unwritable, so no parser can point at it. Smart casts are inference too: the tree shows an is check and a later use, not a narrowed type.
val a: String? = fromJava() // really String!if (x is Foo) x.bar() // smart cast: inference
That last cell is the honest boundary of this product and it is worth stating twice: a parser can tell you every declared nullable type, and cannot tell you where platform types leak in, because they are invisible in source. If your question is “where will this Kotlin/Java codebase throw a null pointer exception”, the answer needs the compiler frontend, not a parser.
What the engine is run against
Fifteen automated tests is a small suite and we are not going to describe it as extensive. The more meaningful evidence is the corpus the checks are configured to parse — and one entry in it is unusually hard to argue with.
JetBrains/kotlinThe Kotlin compiler’s own sources.
One line that explains why the Java boundary matters
Nested interpolation is the construct we are proudest of parsing, and the file annotation on the first line is the reason this engine exists at all. Both are in the same twenty lines.
Input — interpolation inside a lambda inside interpolation
Report.kt
@file:JvmName("Reports") // Java callers see a class named Reportspackage com.example.report
sealed interface Result<out T>
data class Ok<T>(val value: T) : Result<T>
data class Err(val cause: Throwable) : Result<Nothing>
infix fun String?.orElse(fallback: String): String = this ?: fallbackclass Report(private val rows: List<Row>, val owner: Owner?) {
val summary: String by lazy { render() } // delegated propertyprivate fun render(): String {
val active = rows.filter { it.qty > 0 } // trailing lambda, implicit itreturn"""|Report for ${owner?.name orElse "unknown"}| rows: ${active.size} of ${rows.size}| top: ${active.maxByOrNull { it.qty }?.let { "${it.sku} (${it.qty})" } ?: "—"}""".trimMargin()
}
fun `total across all rows`(): Int = rows.sumOf { it.qty }
}
The top: line is an interpolation containing a lambda containing a string containing two more interpolations. All of it is parsed as expressions, not held as opaque text — which is the concrete contrast with a pre-PEP-701 f-string in our Python engine. Meanwhile line 1 changes the Java-visible name of this file’s class, so a call graph built only from the Kotlin source will not match what a Java caller sees.
WHY THIS ONE, AND WHEN NOT
The compiler frontend is better at Kotlin. It is no good at Java.
The Kotlin compiler frontend, PSI and the Analysis API are free, open source and written by the people who define the language. If Kotlin is the only language in scope, use them, and we will tell you so on the first call.
Almost no Kotlin codebase is only Kotlin. Android and JVM estates are Kotlin and Java, compiled together in one module, seeing each other’s declarations. A dependency map, an impact analysis or a migration that stops at the language boundary is not useful — and that boundary is exactly where the interesting failures live: platform types, @JvmName and @JvmStatic changing the generated signature, property and getter duality, name mangling for internal members, checked exceptions that exist on one side and not the other.
We have both engines, and they produce the same AST shape. Every Strumenta engine is built on Starlasu, so a Kotlin tree and a Java tree have the same traversal model and the same API — one visitor crosses the boundary. That is not a claim the Kotlin compiler frontend can make, and it is not one javaparser can make either.
Every engine supports LionWeb, so models interchange with LionWeb-compliant tooling instead of being trapped inside a process holding a compiler. And every engine is reachable through bindings from Java, Kotlin, Python, TypeScript and C# — so a cross-language analysis can be hosted in whatever your team already writes.
So: license the Kotlin engine when Kotlin is one language in a multi-language pipeline, typically alongside Java. Do not license it to analyze Kotlin on its own.
The other half of the codebase
one model, one traversal
Java engine → The same AST shape, with symbol resolution on its side.
SQL engine → What the Exposed and JDBC strings actually contain.
COBOL engine → What the JVM services are usually talking to.
A JVM library and a command-line tool, a generated description of the model, and a license with support in it. The engine runs on the widest JVM floor in the catalog.
01The engine
A library and a command-line tool, on Java 8 or later
Kotlin on the JVM, built with ANTLR on Kolasu — the JVM implementation of Starlasu. In process you get the tree and the positioned issue list; on the command line you get the AST serialized as JSON or XML. There is also a snippet entry point that parses a bare expression, which is what a template engine or an editor needs. A license file is registered at start-up; licenses are refreshed from a license service, so tell us early if the engine has to run air-gapped.
library · CLI · expression entry point
02The model
95 typed nodes, positions, comments retained
A typed AST rooted at a compilation unit — imports, package, class and object declarations, properties, methods, top-level and extension functions, constructors, initializers, when clauses, lambdas, type aliases, annotations, delegates and the modifier set. Positions on every node, parents assigned, and comments kept on the hidden channel rather than skipped.
typed AST · positioned · comments kept
03The write path
Templates, a printer, and golden-file round trips
There is a template facility and a code-generation module that prints Kotlin from an AST, with paired source and serialized-tree examples committed as golden files. Kotlin is therefore usable as a generation target as well as a source — a smaller capability than the Java engine’s, and we would rather scope it with you than oversell it.
templates · printer · golden files
04The license
Standard, Distribution or Service — support included
Standard for use inside your own organization, Distribution if the engine ships inside a product you sell, Service if it runs behind a service you operate. Support is part of the license. LionWeb export and bindings from Java, Kotlin, Python, TypeScript and C# come with it.
three tiers · LionWeb · five bindings
What the model is good for
Every declaration, branch and loop as a typed, positioned node — counted by a traversal you write once.
Module size, class counts and decision points fall straight out of the tree. Resolved dependency graphs and call graphs do not, because there is no symbol resolution here. We do not ship a metrics dashboard, and we are not going to describe imports as a dependency analysis.
What to discuss before you license
Eight things, which is more than most pages in this catalog carry. This is a younger engine than our COBOL, RPG or SAS ones and the honest register is different.
Kotlin 1.9 is the ceiling. Kotlin 2.x is current. The 2.0 release was a compiler-frontend rewrite rather than a syntactic overhaul, but 2.x has added syntax and none of it is in this grammar.
.kts scripts are not supported. Only .kt is wired up. Gradle build files, settings files and custom scripting hosts will not parse. The script rule is in the grammar already, so this is scoped work rather than research — but today it is a no.
There is no symbol resolution. No type inference, no smart-cast information, no call graph across files. You can extract imports and syntactic references; you cannot get resolved dependencies, and we are not going to call one the other.
The AST does not yet cover everything the grammar parses. The transformer from parse tree to AST carries a number of unhandled branches. In practice that means some constructs parse and then have no typed node behind them. Ask us which, for the constructs you care about — that is a specific answer we can give.
data and inline have no dedicated node. They are carried as modifiers on the declaration rather than as first-class concepts, unlike extension functions and sealed types.
Platform types are invisible.T! has no source syntax, so nothing that reads source can surface it. If your question is about null safety across the Java boundary, you need the compiler, not a parser.
Multiplatform and generated code are out of scope.expect/actual source-set resolution is a build concern; code produced by compiler plugins — Compose, serialization, annotation processors — is not in the source and therefore not in the tree. Internal DSLs are ordinary Kotlin syntax and are not analyzed as DSLs.
The license is a file, refreshed from a service. The engine registers a license file at start-up and the license expires on the order of a month. In a connected environment that is invisible; in an air-gapped one it needs designing for, so raise it in the first call rather than the last.
What we will ask you
Is Kotlin the only language in scope? If it is, we will point you at the compiler frontend and the Analysis API.
Is there Java in the same codebase? That is the case this engine is for.
Do you need .kts build scripts parsed?
What Kotlin version is the source, and does anything depend on 2.x syntax?
Do you need symbol resolution, type inference or a call graph?
Is Kotlin a source to analyze or a target to generate?
Android, JVM, Native, JS or multiplatform?
What language is your tool written in?
Straight answers
Does it parse Gradle build scripts?
No. Kotlin has two source forms with two different start rules: a Kotlin file allows only declarations at top level, a Kotlin script allows statements. They produce structurally different trees, and only the file form is wired up in what we ship — so build.gradle.kts and settings.gradle.kts will not parse. The script rule is present in the official grammar we use, so enabling it is scoped work rather than research. Tell us if you need it.
Which Kotlin version does it cover?
Kotlin through 1.9. Kotlin 2.x is current; the 2.0 release was a compiler-frontend rewrite rather than a syntactic overhaul, but 2.x has added syntax — context parameters, guard conditions in when, non-local break and continue among them — and none of that is in the grammar today.
Can it build a call graph or a dependency graph?
Not on its own, and we would rather say so than let the word “dependency” do the work. There is no symbol-resolution module here: you get imports and syntactic references, positioned, which is enough for an inventory and not enough for a resolved call graph. If that is what you need in Kotlin specifically, the Kotlin Analysis API is the honest answer.
Whose grammar is it?
JetBrains’. Kotlin’s formal specification publishes its grammar as an ANTLR file, and the reference grammar page is generated from it. We build on that file, with our own additions for parsing large codebases in one process without accumulating parser state. It means the definition of legal Kotlin is a versioned document you can diff, rather than something reverse-engineered from compiler behavior.
How heavily tested is it?
Lightly, by the standards of this catalog: fifteen automated tests, which is roughly a twentieth of what the Java engine carries. We are not going to call that extensive. The stronger evidence is the corpus the checks are configured against, which includes okhttp, Android architecture samples, a large real application, and the JetBrains Kotlin compiler’s own sources.
Send us the mixed moduleKotlin and Java in one folder
The interesting question is where the Java starts.
Send us a module with both languages in it and tell us what you are trying to learn from it — an inventory, an impact analysis, a migration. We will come back with what one model across both would give you and what it would not. If Kotlin is all you have, we will point you at JetBrains’ own tooling and say so plainly.