Summary
The Kotlin pattern at src/CodeIndex/Indexer/SymbolExtractor.cs:189 captures companion object declarations with a zero-width-permitted \w* name group:
// Companion object / コンパニオンオブジェクト
new("class", new Regex(@"^\s*companion\s+object\s*(?<name>\w*)", RegexOptions.Compiled), BodyStyle.Brace),
The idiomatic Kotlin spelling is the anonymous companion, companion object { ... } (no trailing identifier). With \w* allowing zero characters, the row still fires on that form, producing a class symbol whose name column is the empty string. That empty-named row shows up directly in symbols / outline / inspect output as a blank line between the enclosing class and the real nested symbols, and any attempt to dereference it (definition "", references "") is user-hostile and impossible to spell correctly on the CLI.
Named companions (companion object Factory) work fine — only the anonymous form — which is the form seen in 90%+ of real Kotlin code (e.g. every standard-library companion, Compose companion object Saver, Ktor Plugin.Key, Android Fragment.newInstance()).
Repro
CDIDX=/root/.local/bin/cdidx
mkdir -p /tmp/dogfood/kt-companion && cat > /tmp/dogfood/kt-companion/K.kt <<'EOF'
class Widget {
companion object {
const val MAX = 100
fun create(): Widget = Widget()
}
}
class Named {
companion object Factory {
fun build(): Named = Named()
}
}
object Singleton {
fun doThing() {}
}
EOF
"$CDIDX" index /tmp/dogfood/kt-companion --rebuild
"$CDIDX" symbols --db /tmp/dogfood/kt-companion/.cdidx/codeindex.db
Observed:
class K.kt:2-5 ← empty name!
class Factory K.kt:9-11
property MAX K.kt:3
class Named K.kt:8-12
class Singleton K.kt:14-16
class Widget K.kt:1-6
function build K.kt:10-16
function create K.kt:4-12
function doThing K.kt:15
(9 symbols in 1 files)
The first row has a blank name. Its range K.kt:2-5 correctly brackets the companion object { ... } block, so the regex fires and the body-range extractor works — only the <name> capture is empty, which then propagates into the database as-is.
Expected: either (a) the anonymous companion is captured as class Companion (the Kotlin default name, visible via reflection as Widget.Companion per the Kotlin spec), or (b) the capture is skipped when the name group is empty.
Suspected root cause
src/CodeIndex/Indexer/SymbolExtractor.cs:189:
new("class", new Regex(@"^\s*companion\s+object\s*(?<name>\w*)", RegexOptions.Compiled), BodyStyle.Brace),
\w* accepts zero characters. No downstream caller checks for empty name before persisting the row (DbWriter.cs UPSERTs whatever comes in; the symbol-insertion path does not filter blank names).
For reference, the same file emits property MAX K.kt:3 correctly, which suggests the patterns around line 200 (val / var) require a non-empty name via \w+. Line 189 is the lone \w* offender in the Kotlin row set.
Suggested direction
Two options, not mutually exclusive:
1. Minimal fix — require a name, default to Companion:
// Companion object / コンパニオンオブジェクト
// Anonymous companions use the Kotlin-default name "Companion" (accessible as <EnclosingClass>.Companion).
// 無名 companion は Kotlin の既定名 "Companion" を採用する。
new("class",
new Regex(@"^\s*companion\s+object(?:\s+(?<name>\w+))?", RegexOptions.Compiled),
BodyStyle.Brace),
Then the surrounding extractor loop (or a tiny post-process) substitutes Companion when the optional capture group is missing. The substitution has to happen in the symbol-building code because the current pipeline inserts whatever <name> produces verbatim.
2. Defensive fix — skip emission on empty name globally:
In whichever method iterates PatternCache and materialises SymbolRecords (the pattern-application loop around SymbolExtractor.cs:400–500), add an early if (string.IsNullOrWhiteSpace(name)) continue; guard so no other language's row with a similar \w* oversight can leak blank-named symbols into the DB. This is cheap insurance on top of (1) and protects against future regex regressions.
Recommend doing both: (1) to get the right answer on Kotlin specifically (anonymous companion = Companion), (2) to harden the extractor against any future \w* slip.
Why it matters
symbols --lang kotlin, outline, inspect, map all surface blank-named entries to AI consumers. An AI asked "summarize Kotlin classes in this repo" gets a symbol list with holes in it and no way to interrogate the hole.
definition Companion (the Kotlin-default companion access path) fails because no symbol has that name.
unused reports the blank-named row as a detached symbol with no callers, introducing spam.
- Anonymous companions are ubiquitous. Kotlin's own stdlib, AndroidX, Compose, Ktor, Arrow-kt, Exposed, Gradle Kotlin DSL, and nearly every Kotlin Multiplatform codebase use
companion object { ... } as the default form. Named companions (companion object Factory) exist but are the minority.
Cross-language note
Kotlin-specific. Dart extensions (extension on T { ... }) have the same optional-name shape, but the existing Dart row at SymbolExtractor.cs:311 requires \w+ between extension and on, so anonymous extensions drop entirely (a different bug: silent drop rather than blank-named capture). Swift extensions are similar — worth a follow-up probe.
Scala 3 anonymous given definitions (given Ordering[Int] = ...) have a similar optional-name pattern but Scala 3 graph support is not present yet, so out of scope.
Scope
src/CodeIndex/Indexer/SymbolExtractor.cs:189 — require \w+ with optional capture, and either emit Companion as the default name or skip emission when absent.
- Pattern-application loop in the same file — add an empty-name guard as defensive insurance.
tests/CodeIndex.Tests/SymbolExtractorTests.cs — fixtures: (a) companion object { ... } captured as class Companion (or skipped, depending on chosen fix), (b) companion object Factory { ... } still captured as class Factory, (c) verify no blank-named rows land in the DB.
Related
Environment
- cdidx v1.10.0 at
/root/.local/bin/cdidx (trimmed release build installed via install.sh).
- Platform: linux-x64 container.
- Filed from a Claude Code cloud session per
CLOUD_BOOTSTRAP_PROMPT.md.
Summary
The Kotlin pattern at
src/CodeIndex/Indexer/SymbolExtractor.cs:189capturescompanion objectdeclarations with a zero-width-permitted\w*name group:The idiomatic Kotlin spelling is the anonymous companion,
companion object { ... }(no trailing identifier). With\w*allowing zero characters, the row still fires on that form, producing aclasssymbol whosenamecolumn is the empty string. That empty-named row shows up directly insymbols/outline/inspectoutput as a blank line between the enclosing class and the real nested symbols, and any attempt to dereference it (definition "",references "") is user-hostile and impossible to spell correctly on the CLI.Named companions (
companion object Factory) work fine — only the anonymous form — which is the form seen in 90%+ of real Kotlin code (e.g. every standard-library companion, Composecompanion object Saver, KtorPlugin.Key, AndroidFragment.newInstance()).Repro
Observed:
The first row has a blank name. Its range
K.kt:2-5correctly brackets thecompanion object { ... }block, so the regex fires and the body-range extractor works — only the<name>capture is empty, which then propagates into the database as-is.Expected: either (a) the anonymous companion is captured as
class Companion(the Kotlin default name, visible via reflection asWidget.Companionper the Kotlin spec), or (b) the capture is skipped when the name group is empty.Suspected root cause
src/CodeIndex/Indexer/SymbolExtractor.cs:189:\w*accepts zero characters. No downstream caller checks for emptynamebefore persisting the row (DbWriter.csUPSERTs whatever comes in; the symbol-insertion path does not filter blank names).For reference, the same file emits
property MAX K.kt:3correctly, which suggests the patterns around line 200 (val/var) require a non-empty name via\w+. Line 189 is the lone\w*offender in the Kotlin row set.Suggested direction
Two options, not mutually exclusive:
1. Minimal fix — require a name, default to
Companion:Then the surrounding extractor loop (or a tiny post-process) substitutes
Companionwhen the optional capture group is missing. The substitution has to happen in the symbol-building code because the current pipeline inserts whatever<name>produces verbatim.2. Defensive fix — skip emission on empty name globally:
In whichever method iterates
PatternCacheand materialisesSymbolRecords (the pattern-application loop aroundSymbolExtractor.cs:400–500), add an earlyif (string.IsNullOrWhiteSpace(name)) continue;guard so no other language's row with a similar\w*oversight can leak blank-named symbols into the DB. This is cheap insurance on top of (1) and protects against future regex regressions.Recommend doing both: (1) to get the right answer on Kotlin specifically (anonymous companion =
Companion), (2) to harden the extractor against any future\w*slip.Why it matters
symbols --lang kotlin,outline,inspect,mapall surface blank-named entries to AI consumers. An AI asked "summarize Kotlin classes in this repo" gets a symbol list with holes in it and no way to interrogate the hole.definition Companion(the Kotlin-default companion access path) fails because no symbol has that name.unusedreports the blank-named row as a detached symbol with no callers, introducing spam.companion object { ... }as the default form. Named companions (companion object Factory) exist but are the minority.Cross-language note
Kotlin-specific. Dart extensions (
extension on T { ... }) have the same optional-name shape, but the existing Dart row atSymbolExtractor.cs:311requires\w+betweenextensionandon, so anonymous extensions drop entirely (a different bug: silent drop rather than blank-named capture). Swift extensions are similar — worth a follow-up probe.Scala 3 anonymous
givendefinitions (given Ordering[Int] = ...) have a similar optional-name pattern but Scala 3 graph support is not present yet, so out of scope.Scope
src/CodeIndex/Indexer/SymbolExtractor.cs:189— require\w+with optional capture, and either emitCompanionas the default name or skip emission when absent.tests/CodeIndex.Tests/SymbolExtractorTests.cs— fixtures: (a)companion object { ... }captured asclass Companion(or skipped, depending on chosen fix), (b)companion object Factory { ... }still captured asclass Factory, (c) verify no blank-named rows land in the DB.Related
typealiasdeclarations are silently dropped by SymbolExtractor — no symbol row at all (sibling of #206 Scala case) #297 — Kotlintypealiasnot extracted. Same row set, different missing feature.public int Count;,private readonly List<int> _items;,public static int GlobalCount;) are not captured as symbols — onlyconstandstatic readonlyare indexed #298 — C# plain fields dropped. Same "Kotlin/C# symbol coverage gap" family.factory/const) andtypedefaliases are silently dropped — entire public API surface of every Dart class becomes invisible #312 — Dart constructors +typedefdropped. Flagged Kotlin secondary constructors as a parallel gap; that half is filed separately as a follow-up.Environment
/root/.local/bin/cdidx(trimmed release build installed viainstall.sh).CLOUD_BOOTSTRAP_PROMPT.md.