Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ The architecture lives in a `groma/` folder as an [Open Knowledge Format 0.2](ht
| [Go](docs/scanners/go/index.md) | ✅ Available |
| [Java (Maven, Gradle)](docs/scanners/java/index.md) | ✅ Available |
| [Scala 3](docs/scanners/scala/index.md) | 🧪 Experimental |
| [Kotlin](docs/scanners/kotlin/index.md) | 🧪 Experimental |
| [Python](docs/scanners/python/index.md) | ✅ Available |
| [Rust](docs/scanners/rust/index.md) | ✅ Available |
| [PHP](docs/scanners/php/index.md) | ✅ Available |
Expand Down Expand Up @@ -154,7 +155,7 @@ Nothing breaks. The architecture stays in your repository as ordinary Markdown i

> ### Which languages does it support?

TypeScript, JavaScript, Angular, React, Vue, C#/.NET, Go, Java, Scala 3, Python, Rust, PHP, Swift, COBOL, and NASM assembly, each through a scanner plugin; see [Languages](#languages). For another language, [write a scanner plugin](docs/scanners/creating-a-plugin.md) or [request one](https://github.com/MrLesk/Groma.md/issues).
TypeScript, JavaScript, Angular, React, Vue, C#/.NET, Go, Java, Scala 3, Kotlin, Python, Rust, PHP, Swift, COBOL, and NASM assembly, each through a scanner plugin; see [Languages](#languages). For another language, [write a scanner plugin](docs/scanners/creating-a-plugin.md) or [request one](https://github.com/MrLesk/Groma.md/issues).

## Experimental

Expand Down
148 changes: 148 additions & 0 deletions backlog/tasks/task-578 - Add-an-experimental-Kotlin-scanner.md

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 7 additions & 2 deletions docs/scanners/creating-a-plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ and are not listed.
It has no `members` list. Programs are Code declarations, not C4 containers;
nested programs are omitted.
- `kind: 'type'` is a top-level class, interface, struct, record, enum, trait,
or protocol, a Scala object, or a Go defined type such as `type X struct{}` or `type X int`.
or protocol, a Scala or Kotlin object, or a Go defined type such as `type X struct{}` or `type X int`.
A named type whose form is a function, such as a C# `delegate` or a Go
`type X func(...)`, is a type with an empty `members` list. Type aliases are
never listed: TypeScript `type X = ...`, Go `type X = Y`, and Rust
Expand All @@ -173,6 +173,7 @@ and are not listed.
declaration is listed separately, including overloads and TypeScript overload
signatures. Scala extension groups keep their enclosing scope: their methods
are top-level functions or members of the declaring type or object.
Functions of a Kotlin companion object are members of the enclosing type.
Constructors use their source name, such as `constructor`,
`__init__`, `__construct`, or the type name.
- Fields, properties, property signatures (even with a function type),
Expand Down Expand Up @@ -216,6 +217,7 @@ follows; a dash means the language has no such case.
| Vue | As TypeScript in `<script>` | As TypeScript | - | As TypeScript; every `<script setup>` top-level declaration |
| Java | `public`; interface members without a modifier | `protected` | No modifier elsewhere (package access) | `private` |
| Scala | No access modifier | `protected`, `protected[scope]` | `private[scope]` | `private`, `private[this]` |
| Kotlin | `public`; default except constructors and inherited overrides | `protected`; sealed constructors by default | `internal` | `private`; enum constructors by default |
| C# | `public`; interface members without a modifier | `protected`, `protected internal`, `private protected` | `internal`; top-level types without a modifier | `private`; other members without a modifier |
| Go | Names starting with an upper-case letter | - | Other names | - |
| Rust | `pub`. Methods in a trait definition take the trait's visibility, and methods in a trait `impl` are `public` | - | `pub(crate)`, `pub(super)`, `pub(in path)` | No `pub`, `pub(self)` |
Expand All @@ -227,7 +229,10 @@ TypeScript re-exports from other files (`export { name } from '...'`) do not
change any declaration's visibility. Python visibility comes from names alone;
`__all__` does not change it. A JavaScript `.mjs` or `.cjs` file is a module
whatever it contains. A Swift member of a protocol or an extension takes that
declaration's access when it states none of its own.
declaration's access when it states none of its own. A Kotlin override without an
access modifier inherits access from its base declaration. When selected source
cannot establish that access, the Kotlin scanner omits only that override member
from the outline; its file, symbols and operation evidence remain available.

## Source file listing

Expand Down
1 change: 1 addition & 0 deletions docs/scanners/discovery.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ depth. These rules are declared by the current official plugins:
| `pom.xml` | Maven project and literal `java.version`, `maven.compiler.release`, or `maven.compiler.source` values; `org.springframework.boot` is a framework clue |
| `build.gradle`, `build.gradle.kts` | Gradle build script and the first literal `JavaLanguageVersion.of(...)` or `JavaVersion.VERSION_...` value, when present; Java sources are not verified |
| `*.scala` | Scala source; compiler version and build configuration are unverified |
| `*.kt` | Kotlin source; compiler version and build configuration are unverified |
| `*.csproj` | C# project and literal `TargetFramework` or `TargetFrameworks` values |
| `pyproject.toml` | Python project configuration and literal `project.requires-python`, when present |
| `setup.py`, `setup.cfg`, `requirements.txt` | Python project or dependency declaration |
Expand Down
1 change: 1 addition & 0 deletions docs/scanners/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ authored relationships remain intact.
- [Python](python/index.md)
- [PHP](php/index.md)
- [Scala 3](scala/index.md)
- [Kotlin](kotlin/index.md)
- [COBOL](cobol/index.md)
- [NASM assembly](nasm/index.md)
- [C#/.NET](dotnet-csharp/index.md)
Expand Down
143 changes: 143 additions & 0 deletions docs/scanners/kotlin/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# Kotlin scanner

The experimental Kotlin scanner parses selected Kotlin source with the Kotlin
compiler's own parser. Its package includes the compiler jars and a Java runtime.
Scanning and source outlines need no installed Java, Kotlin, Gradle, Maven,
project dependencies, build, or network access.

From an initialized project:

```sh
groma scanner add @groma/scanner-kotlin
groma scan
```

## Source selection

The scanner reads the `.kt` files selected by Groma's
[include and exclude lists](../index.md#selecting-source-files), wherever they
live in the repository. Defaults exclude `build/` and `.gradle/`, but keep a
package folder named `build` under a source set's `kotlin/` or `java/` root,
such as `src/main/kotlin/shop/build/`. They also exclude, in every module,
`src/test/` and the other test source sets: `src/*Test/` such as
`src/commonTest/` and `src/androidTest/`, and their variants and fixtures such
as `src/testDebug/`, `src/androidTestDebug/` and `src/testFixtures/`.
Adjust those lists to scan test code or a custom source layout.

It does not evaluate Gradle or Maven builds, discover modules or source sets,
inspect dependencies, or read generated source. Kotlin scripts (`.kts`) are not
read. A `.kt` file is a discovery clue; it does not prove a compiler version.
Files are parsed with the parser of `kotlin-compiler-embeddable` 2.4.21. Syntax
newer than that parser supports is not promised.

## Evidence and outlines

The TypeScript entry point (`src/index.ts`) filters the selected paths and
`src/adapter.ts` starts the bundled worker (`worker/md/groma/scanner`). Running
a worker on the bundled Java runtime and turning its outline into Code entries
is shared with the Scala scanner in `plugins/scanners/jvm-worker.ts`. The
worker parses each file without analyzing it and reports:

- **Symbols**: top-level classes, interfaces, objects, enum classes and
functions. An extension function is a function under its own name. A top-level
`val` initialised directly with a lambda or an anonymous function is a
function too; a `var` is not, because it can be reassigned.
- **Operations**: functions with a body, secondary constructors, and such
function-valued properties, with original source positions. A member is named
by its type, such as `Orders.place`. A secondary
constructor is `Orders.constructor`. Functions of a companion object belong to
the enclosing type, as `Orders.create`, matching the shared outline convention. Unlike the outline, operations also cover the functions of
nested types, as `Orders.Nested.run`, and function-valued properties declared
in a type. A position is the declaration's start after its KDoc and comments.
- **Invocations**: call expressions belonging to the operation, parameter
defaults included, at the start of its receiver when it has one. Bodies of
nested functions, lambdas and local-class methods are not attributed to the
enclosing operation. A named function value keeps its own direct calls;
anonymous-object initialization still runs at the enclosing expression.
Calls remain unresolved because syntax alone
cannot prove their targets: `Orders()` may construct a type or call a function.
A secondary constructor's `this(...)` or `super(...)` delegation is not a
call, though calls in its arguments are. Operators, infix calls such as
`1 to 2`, and property accessors are not reported. Initializer blocks do not
become separate operations.

The scanner supplies no body fingerprints for duplicate-code findings and no
HTTP facts. A parse or read failure rejects the whole observation and names the
source file, with the line of a syntax error. The worker runs with a 512 MB stack for long generated
expressions; a file nested deeper than that fails the same way. There is no successful partial scan. In a file with CRLF
line endings or a leading byte order mark, positions and lines still refer to the file on disk.

The same parser supplies [source outlines](../creating-a-plugin.md#source-outline):
top-level types and objects with their functions, and top-level functions or
function-valued properties. A type's members also include its constructors,
named `constructor`: the primary constructor when the class header declares one,
then each secondary constructor. Companion-object functions are members of the
enclosing type. Other nested types, properties, and type aliases are omitted.
Code links use bare symbol names; a type link does not mark its members.

Explicit visibility is preserved. Without a modifier, constructors are
`protected` in sealed classes and `private` in enums; other declarations default
to `public`, except overrides that inherit their access.

`Visibility.kt` finds inherited access in the requested files and the selected
source context supplied by Groma. It follows enclosing types, same-package
names, explicit imports (including aliases) and qualified type names. It does
not resolve wildcard imports or type aliases. Overloads are compared by name,
parameter count and presence of an extension receiver; all plausible declarations
must agree on access. If a base is missing, duplicated across source sets, or
ambiguous, that override member is omitted from the outline. Explicitly visible
members and the containing type remain. File inventory, symbols and operations
are unchanged. This keeps unknown access from being reported as public without
requiring project dependencies. See Kotlin's [visibility rules](https://kotlinlang.org/docs/visibility-modifiers.html).

Source listing filters the supplied candidates by `.kt` without parsing
files or starting the worker.

## Architecture ownership

The worker's single source root groups evidence for this scan. A Kotlin
package, object, file, Gradle module, or source set does not imply a C4 system
or container. Groma core owns architecture boundaries and interprets operation
evidence through the [shared scanner contract](../creating-a-plugin.md).

Architecture remains ordinary OKF Markdown with Code links. Other Markdown
readers can follow those links without the scanner. Groma interprets the source
evidence through its existing scanner and component model; Kotlin adds no new
architecture kind or metadata.

## Maintainer build

Maintainers need Bun, a JDK with `java` and `jlink`, and network access:

```sh
bun plugins/scanners/kotlin/build.ts
```

The build downloads the pinned compiler jars from Maven Central, rejects a jar
whose SHA-256 differs from the one recorded in `build.ts`, and compiles the
worker with that same compiler, so it needs no installed Kotlin compiler or
Gradle. This build-time tooling is not part of scanning. The result is
`plugins/scanners/kotlin/dist/package`; use this local package when working on
the scanner before publication. To change the parser version, change `kotlin`
and the jar checksums in `build.ts`, and the version named in
`THIRD-PARTY-NOTICES.txt` and on this page.

`build.ts` owns the worker compilation; `plugins/scanners/jvm-package.ts`
assembles the package around it, as it does for the Scala scanner.

The existing scanner release workflow builds each supported host and combines
the runtimes. The package contains bundled JavaScript, `dist/worker.jar`, the
unmodified compiler jars under `dist/lib`, a runtime under
`dist/<host>/runtime`, and third-party notices. The runtime's licenses remain in
its `legal/` directory. There are no install scripts or scan-time downloads.

The compiler's parser links desktop classes when it loads, so the runtime
includes the `java.desktop` module. That makes each host runtime about 77 MB
(24 MB compressed), beside about 60 MB of compiler jars shared by all hosts.

The Kotlin test builds an isolated package and checks selection, declarations,
calls, positions, parse failure, and outlines. The shared fresh-checkout test
also runs the relocated package with an empty home and no language tools on
`PATH`, checks repeatability, and checks that the checkout stays unchanged.

Real-project qualification is recorded in [Kotlin scanner validation](validation.md).
45 changes: 45 additions & 0 deletions docs/scanners/kotlin/validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Kotlin scanner validation

The experimental scanner was tested on 2026-10-11 using its built, relocated
package on macOS ARM64, Bun 1.4.3 and the bundled Java runtime. Each checkout
was pinned to the commit below. No project build or dependency installation ran.

| Project | Commit | Selected `.kt` files | Operations | Unresolved calls |
| --- | --- | ---: | ---: | ---: |
| [Coil](https://github.com/coil-kt/coil) | [`8efac6f9`](https://github.com/coil-kt/coil/tree/8efac6f9d34716e5bbdd08a7e31c6e075dfca542) | 347 | 1,351 | 2,813 |
| [Koin](https://github.com/InsertKoinIO/koin) | [`dc86ef8d`](https://github.com/InsertKoinIO/koin/tree/dc86ef8dd8fbe8564fb7453c03f5b738da3450bb) | 357 | 1,262 | 3,221 |
| [Arrow](https://github.com/arrow-kt/arrow) | [`3373cce8`](https://github.com/arrow-kt/arrow/tree/3373cce8a0658c95857d748210547d6b50dd5e9d) | 253 | 2,552 | 4,640 |
| [Now in Android](https://github.com/android/nowinandroid) | [`a49ed253`](https://github.com/android/nowinandroid/tree/a49ed253d75e61a2b6ab80a8da677b57437b08eb) | 253 | 526 | 1,156 |
| [Ktor](https://github.com/ktorio/ktor) | [`4717dd5f`](https://github.com/ktorio/ktor/tree/4717dd5fe8270a40fc97ba0b03bb63f722717786) | 2,432 | 13,513 | 35,580 |

All five passed, covering **3,642 files, 19,204 operations and 47,410 calls**.
Selection used Groma's normal scanner include/exclude rules. Ktor has custom
folders such as `jvm/test`, which remain selected by the defaults; these counts
are selected source, not production-only source. Adjust the selection for a
project's intended architecture view.

## Checks performed

The scanner ran with an empty home directory and `PATH=/nonexistent`, using its
bundled runtime. JavaScript network requests were blocked. For each project:

- File inventory and source listing matched the selected paths exactly.
- Two scans produced identical observations.
- Operation and call offsets were within the original UTF-16 source; call lines
matched those offsets and every call named an existing caller operation.
- Calls remained unresolved with no guessed targets.
- Outlines contained only selected files, with valid source lines and visibility
values from the shared outline contract.
- Git status was unchanged after scanning and reading outlines.

These checks establish parsing, delivery and structural consistency on real
source. They do not independently prove every inferred declaration or call.
The authored regression fixture in `test/fixtures/kotlin-source/scopes/` checks
call ownership and visibility against known answers: deferred nested bodies and accessors,
immediate object initialization, sealed/enum constructors, inherited access,
cross-file imports, and omission of overrides whose access cannot be proved.

The isolated-package test is `test-bun/kotlin-scanner.test.ts`. The shared
`test-bun/scanner-fresh-checkout.test.ts` also verifies scanning with no project
dependencies or installed language tools. See the [scanner documentation](index.md)
for the supported evidence and source-only limits.
1 change: 1 addition & 0 deletions groma/scanners.json
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@
"/examples/",
"/plugins/scanners/*/build.ts",
"/plugins/scanners/framework-package.ts",
"/plugins/scanners/jvm-package.ts",
"/plugins/scanners/rust/notices.ts"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
type: C4 Component
title: Kotlin source scanner
status: stable
groma:
id: kotlin-src-index
parent: cli
code:
- scanner: typescript
file: plugins/scanners/kotlin/src/index.ts
- scanner: typescript
file: plugins/scanners/kotlin/src/adapter.ts
group: Language scanners
technology: TypeScript, Kotlin, Kotlin compiler
description: Reads selected Kotlin declarations, outlines and uncertain call evidence.
---

Receives Groma's selected .kt paths and runs the bundled Kotlin worker with its bundled Java runtime. The worker parses original source with the Kotlin compiler's parser and reports declarations, operations and unresolved call sites, or rejects the observation when a selected file cannot be read or parsed. The same parser supplies Code outlines. Scanning never evaluates Gradle or Maven builds or needs project dependencies. Core and human curation own architecture boundaries and relationships.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ groma:
file: plugins/scanners/scala/src/index.ts
- scanner: typescript
file: plugins/scanners/scala/src/adapter.ts
- scanner: typescript
file: plugins/scanners/jvm-worker.ts
group: Language scanners
technology: TypeScript, Scala, Scalameta
description: Reads selected Scala 3 declarations, outlines and uncertain call evidence.
Expand Down
Loading
Loading