Summary
C# positional records (record Point(int X, int Y), readonly record struct Vec3(double X, double Y, double Z)) desugar to auto-properties with init-only setters. Semantically, X / Y / Z ARE the record's public API — they're what every caller reads and what record's equality / with-expressions are built on. But the C# symbol extractor indexes only the record shell and drops every positional component.
Same bug in Java. Java 16+ records record Point(int x, int y) expose auto-accessors x() / y() with the same semantic role. cdidx captures only the record shell; definition x on a Java record file returns nothing.
The user impact is the same in both languages: anyone asking an AI agent "find the definition of X" on a typical immutable-data codebase gets No symbols found. for every field that lives on a positional record.
Repro
C#
CDIDX=/root/.local/bin/cdidx
mkdir -p /tmp/dogfood/csrec
cat > /tmp/dogfood/csrec/R.cs <<'EOF'
namespace App;
public record Point(int X, int Y);
public readonly record struct Vec3(double X, double Y, double Z);
public record Animal(string Name);
public record Dog(string Name, string Breed) : Animal(Name);
public record Options(string Host, int Port) { public bool UseTls { get; init; } = true; }
public record Container<T>(T Value, int Count) where T : class;
EOF
"$CDIDX" /tmp/dogfood/csrec --db /tmp/dogfood/csrec.db
"$CDIDX" symbols --db /tmp/dogfood/csrec.db --limit 100
Actual output (abridged): only Point, Vec3, Animal, Dog, Options, Container show up, plus the UseTls body property. Not a single X, Y, Z, Name, Breed, Host, Port, Value, Count. definition Host --exact → No symbols found..
Java
mkdir -p /tmp/dogfood/javarec
cat > /tmp/dogfood/javarec/R.java <<'EOF'
package com.example;
public record Point(int x, int y) {}
public record Range(int low, int high) {
public Range {
if (low > high) throw new IllegalArgumentException();
}
}
EOF
"$CDIDX" /tmp/dogfood/javarec --db /tmp/dogfood/javarec.db
"$CDIDX" symbols --db /tmp/dogfood/javarec.db
Actual: class Point, class Range rows — no x, y, low, high. The Java record's compact constructor public Range { ... } is also dropped (separate issue; see companion bug report).
Suspected root cause (from reading the source)
src/CodeIndex/Indexer/SymbolExtractor.cs:81 (C# class / record pattern):
new("class", new Regex(
@"^\s*(?:(?<visibility>...)?\s+)?(?:(?:static|partial|abstract|sealed|readonly|file|new|unsafe)\s+)*"
+ @"(?:record\s+class\s+|record\s+|class\s+)(?<name>\w+)",
...), BodyStyle.Brace, "visibility"),
The pattern captures the record's name but does nothing with the parenthesized parameter list that follows. There is no post-processing step to parse (int X, int Y) and emit synthetic auto-property symbols for each positional component.
src/CodeIndex/Indexer/SymbolExtractor.cs:167 (Java record pattern):
new("class", new Regex(
@"^\s*(?<visibility>public|private|protected)?\s*(?:(?:static|final|abstract|sealed|non-sealed|strictfp)\s+)*record\s+(?<name>\w+)",
...), BodyStyle.Brace, "visibility"),
Same structure — parameter list is not examined.
Suggested direction
Add a second pass specific to record lines (both languages):
- Match the primary-constructor parameter list on the same line (or across continuation lines if the declaration wraps).
- For each
<ParameterType> <ParameterName>, emit a synthetic property symbol at the same line number, with the record's name stored as the enclosing symbol.
Minimal C# sketch (one additional recognizer, invoked only when the outer pattern matched a record):
// After a record shell match, scan "(...)" for parameters
// For each "Type Name[,)]", emit: new SymbolRecord("property", Name, recordLine, ..., enclosing: recordName)
Java: same approach; parameter naming convention is lowercase but the extraction is identical.
This is a one-pattern / one-post-processor change per language, and it lights up what users already think of as "the fields of a record."
Why it matters
- Positional records are the idiomatic immutable-data form in modern C# (from C# 9 onward) and Java (from Java 16 onward). Large codebases routinely have hundreds of them.
- Every primary-constructor component is both a field and an auto-property — it's the first thing AI agents search for when navigating a record. The current behavior returns
No symbols found. for every such field.
hotspots, unused, and map all systematically underreport record components today.
- Because the bug is silent, an AI client looking at its
outline for a record sees an empty body and may conclude the record "has no members," when in reality it has 3 public auto-properties.
Scope
src/CodeIndex/Indexer/SymbolExtractor.cs — add record-parameter post-processing for both csharp and java language arrays.
tests/CodeIndex.Tests/SymbolExtractorTests.cs — fixtures for each language: positional record, positional record with additional body members, generic record, inheriting record.
DEVELOPER_GUIDE.md language-pattern reference table (both C# and Java rows).
Cross-language note
Same bug applies to Java — Java 16+ records expose x()-style accessors that are semantically identical to C# X { get; init; }. Any fix here should cover both languages in the same PR. (Kotlin data class primary parameters would be another natural extension, but kotlin extraction has different mechanics; keep that out of scope.)
Related
Environment
- cdidx: v1.10.0 (tarball from GitHub releases).
- Platform: linux-x64 container.
- Filed from a cloud Claude Code session per
CLOUD_BOOTSTRAP_PROMPT.md.
Summary
C# positional records (
record Point(int X, int Y),readonly record struct Vec3(double X, double Y, double Z)) desugar to auto-properties with init-only setters. Semantically,X/Y/ZARE the record's public API — they're what every caller reads and whatrecord's equality /with-expressions are built on. But the C# symbol extractor indexes only the record shell and drops every positional component.Same bug in Java. Java 16+ records
record Point(int x, int y)expose auto-accessorsx()/y()with the same semantic role. cdidx captures only the record shell;definition xon a Java record file returns nothing.The user impact is the same in both languages: anyone asking an AI agent "find the definition of
X" on a typical immutable-data codebase getsNo symbols found.for every field that lives on a positional record.Repro
C#
Actual output (abridged): only
Point,Vec3,Animal,Dog,Options,Containershow up, plus theUseTlsbody property. Not a singleX,Y,Z,Name,Breed,Host,Port,Value,Count.definition Host --exact→No symbols found..Java
Actual:
class Point,class Rangerows — nox,y,low,high. The Java record's compact constructorpublic Range { ... }is also dropped (separate issue; see companion bug report).Suspected root cause (from reading the source)
src/CodeIndex/Indexer/SymbolExtractor.cs:81(C# class / record pattern):The pattern captures the record's name but does nothing with the parenthesized parameter list that follows. There is no post-processing step to parse
(int X, int Y)and emit synthetic auto-property symbols for each positional component.src/CodeIndex/Indexer/SymbolExtractor.cs:167(Java record pattern):Same structure — parameter list is not examined.
Suggested direction
Add a second pass specific to record lines (both languages):
<ParameterType> <ParameterName>, emit a syntheticpropertysymbol at the same line number, with the record'snamestored as the enclosing symbol.Minimal C# sketch (one additional recognizer, invoked only when the outer pattern matched a record):
Java: same approach; parameter naming convention is lowercase but the extraction is identical.
This is a one-pattern / one-post-processor change per language, and it lights up what users already think of as "the fields of a record."
Why it matters
No symbols found.for every such field.hotspots,unused, andmapall systematically underreport record components today.outlinefor a record sees an empty body and may conclude the record "has no members," when in reality it has 3 public auto-properties.Scope
src/CodeIndex/Indexer/SymbolExtractor.cs— add record-parameter post-processing for bothcsharpandjavalanguage arrays.tests/CodeIndex.Tests/SymbolExtractorTests.cs— fixtures for each language: positional record, positional record with additional body members, generic record, inheriting record.DEVELOPER_GUIDE.mdlanguage-pattern reference table (both C# and Java rows).Cross-language note
Same bug applies to Java — Java 16+ records expose
x()-style accessors that are semantically identical to C#X { get; init; }. Any fix here should cover both languages in the same PR. (Kotlindata classprimary parameters would be another natural extension, butkotlinextraction has different mechanics; keep that out of scope.)Related
classacross Rust / TypeScript / Swift / Go / F# (and silently dropped in Scala) #206 — type-alias classification (same extractor family).propertysymbols (incl. constant patterns producing nonsensical names like0) #208 — C# switch-expression property false positives (same extractor).+,implicit,explicit,this) #213, C# enum members: all classified asfunctionkind, and lowercase-named members (active,pending) silently dropped #214, C#:[assembly: Attr(args)]/[module: Attr(args)]lines falsely indexed as method definitions #219 — other C# extractor bugs from recent dogfooding.Environment
CLOUD_BOOTSTRAP_PROMPT.md.