You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
readonly is a valid property/accessor modifier on C# 8+ struct members (and implicit on readonly struct / readonly ref struct members), but the C# property regex modifier slot omits it. This causes two distinct visible failures from the same root cause:
Every readonly property is silently dropped. All three shapes — expression-bodied (public readonly int X => _v;), auto-property (public readonly int X { get; }), and accessor-body (public readonly int X { get => _v; }) — never surface in symbols / definition / outline / inspect.
Per-accessor readonly get/set => … lines are captured as phantom property get (or property set) rows. In a property where the overall property line is already lost to a separate bug (Allman-style brace on the next line — C#: properties declared with { on the next line (block style) are silently dropped — only same-line { get; set; } is captured #229), the standalone accessor line readonly get => _v; still matches the expression-bodied property regex: readonly is consumed as the returnType and get as the name. A bogus property get appears in the symbol table pointing at the accessor line.
The second effect is independent of #229: even on a file that has zero Allman-style properties, any per-accessor readonly get/set => expr; line on its own emits a phantom property get row. The first effect is independent of #228 (which covers partial, not readonly) and independent of #224 (which covers ref / ref readonlyreturns, not the readonly modifier slot).
readonly methods are captured today because the method regex at SymbolExtractor.cs:94 already includes readonly in its modifier slot — the bug is specifically that the two property regexes at :100 and :103 do not.
Repro
CDIDX=/root/.local/bin/cdidx
mkdir -p /tmp/dogfood/cs-readonly-prop
cat > /tmp/dogfood/cs-readonly-prop/R.cs <<'EOF'namespace Demo;public struct S{ private int _v; // 1) readonly + expression-bodied property — DROPPED public readonly int A => _v; // 2) readonly + auto-property (same-line brace) — DROPPED public readonly int B { get; } // 3) readonly + expression-bodied accessor — DROPPED public readonly int C { get => _v; } // 4) Allman-style Mixed (dropped by #229) … public int Mixed { // 5) … but the accessor line here surfaces as a PHANTOM `property get`. readonly get => _v; set => _v = value; } // Baselines — captured: public int D { get; set; } // plain auto-property public readonly int GetD() => D; // readonly method (captured) public readonly int Sum(int x) => _v + x; // readonly method with args (captured)}EOF"$CDIDX" index /tmp/dogfood/cs-readonly-prop --rebuild >/dev/null
"$CDIDX" symbols --db /tmp/dogfood/cs-readonly-prop/.cdidx/codeindex.db --path R.cs
Observed (actual):
function GetD R.cs:27
property D R.cs:26
function Sum R.cs:28
property get R.cs:20 ← PHANTOM: from `readonly get => _v;`
struct S R.cs:3-29
namespace Demo R.cs:1
definition A / B / C all return No symbols found. on any readonly struct / readonly ref struct codebase that uses explicit readonly markers on properties.
outline of a file like System.Numerics.Vector<T> (heavy with readonly properties) lists ~30–50% of its property surface.
symbols --kind property --count on System.Span<T> / ReadOnlySpan<T> / Memory<T> / any performance-oriented library that prefers readonly struct undercounts by the number of explicit-readonly properties.
symbols get --exact returns phantom matches on any file that uses the readonly get => … accessor modifier.
inspect get can suggest that "get" is a member of the surrounding struct, polluting AI navigation.
Suspected root cause (from reading the source)
src/CodeIndex/Indexer/SymbolExtractor.cs:100 — property with get/set/init:
Match succeeds; symbol recorded as property get. There is no containing-property gate, no "did we just see a { opening this member?" check, so the accessor line is taken to be a top-level property declaration all on its own.
Why both effects share one fix
Adding readonly to the modifier slot in both :100 and :103:
— for the phantom readonly get => _v; line, readonly is now consumed by the modifier slot, then (?<returnType>\w+) must match a non-empty returnType followed by \s+(?<name>\w+)\s*=>\s*. The remaining text is get => _v;, which has no name \s*=> shape (there is no second identifier between get and =>). Match fails; no phantom.
— for public readonly int A => _v;, readonly is now consumed by the modifier slot, leaving int A => _v;. returnType matches int, name matches A, => matches. Match succeeds; property captured.
Both effects fall out of the single one-token addition.
Land the same modifier widening in the indexer regex at :118 (already includes static|virtual|override|abstract|sealed|new — add readonly).
Add an independent guard so per-accessor readonly get/set => expr; lines don't drive the expression-bodied property regex on their own, as a defense-in-depth against future modifier slots creating similar phantoms: either require the returnType to be followed by an identifier distinct from get/set/init, or skip the expression-bodied property regex when the preceding non-blank line ends in {.
Add SymbolExtractorTests.cs fixtures asserting:
All three readonly shapes (expression-bodied, auto-property, accessor-body) produce exactly one property row each with the expected name.
A property with readonly get => _v; on one accessor does NOT produce a property get / property set phantom row.
Regression: plain non-readonly properties still capture.
Swift — uses @MainActor, nonisolated, dynamic before var / let; not captured as properties in the Swift regex row set, but Swift's symbol extractor shape is different enough that the bug does not directly transfer.
Kotlin — var / val with modifiers (open, override, final); the Kotlin row set has its own slot, orthogonal to this bug.
The phantom from accessor line effect does generalize: any language whose per-accessor body syntax (e.g. hypothetical readonly get => x;) coincidentally matches the "expression-bodied property" shape would be susceptible. Today the direct impact is C# only.
Summary
readonlyis a valid property/accessor modifier on C# 8+ struct members (and implicit onreadonly struct/readonly ref structmembers), but the C# property regex modifier slot omits it. This causes two distinct visible failures from the same root cause:readonlyproperty is silently dropped. All three shapes — expression-bodied (public readonly int X => _v;), auto-property (public readonly int X { get; }), and accessor-body (public readonly int X { get => _v; }) — never surface insymbols/definition/outline/inspect.readonly get/set => …lines are captured as phantomproperty get(orproperty set) rows. In a property where the overall property line is already lost to a separate bug (Allman-style brace on the next line — C#: properties declared with{on the next line (block style) are silently dropped — only same-line{ get; set; }is captured #229), the standalone accessor linereadonly get => _v;still matches the expression-bodied property regex:readonlyis consumed as the returnType andgetas the name. A bogusproperty getappears in the symbol table pointing at the accessor line.The second effect is independent of #229: even on a file that has zero Allman-style properties, any per-accessor
readonly get/set => expr;line on its own emits a phantomproperty getrow. The first effect is independent of #228 (which coverspartial, notreadonly) and independent of #224 (which coversref/ref readonlyreturns, not thereadonlymodifier slot).readonlymethods are captured today because the method regex atSymbolExtractor.cs:94already includesreadonlyin its modifier slot — the bug is specifically that the two property regexes at:100and:103do not.Repro
Observed (actual):
Missing:
property A(L8)property B(L11)property C(L14)property Mixed(L17-21) — dropped by C#: properties declared with{on the next line (block style) are silently dropped — only same-line{ get; set; }is captured #229 because{is on the next lineExtra:
property get(L20) — phantom, should not exist.Downstream effects:
definition A/B/Call returnNo symbols found.on anyreadonly struct/readonly ref structcodebase that uses explicitreadonlymarkers on properties.outlineof a file likeSystem.Numerics.Vector<T>(heavy withreadonlyproperties) lists ~30–50% of its property surface.symbols --kind property --countonSystem.Span<T>/ReadOnlySpan<T>/Memory<T>/ any performance-oriented library that prefersreadonly structundercounts by the number of explicit-readonlyproperties.symbols get --exactreturns phantom matches on any file that uses thereadonly get => …accessor modifier.inspect getcan suggest that "get" is a member of the surrounding struct, polluting AI navigation.Suspected root cause (from reading the source)
src/CodeIndex/Indexer/SymbolExtractor.cs:100— property withget/set/init:SymbolExtractor.cs:103— expression-bodied property:Both modifier alternations are
static|virtual|override|abstract|sealed|new|required. Missing:readonly. Compare with the method regex at:94:—
readonlyis there, which is exactly whypublic readonly int GetD() => D;at L27 of the repro is captured.Effect 1 — dropped symbol
Walkthrough for
public readonly int A => _v;against:103:public→ visibility matched.readonlyis not in the alternation → 0 modifiers consumed.(?<returnType>[\w?.<>\[\],:]+)→ matchesreadonly.\s+(?<name>\w+)→ matchesint(name=int).\s*=>\s*→ expects=>but findsA; match fails.The whole line is rejected, no symbol produced.
Effect 2 — phantom symbol
Walkthrough for the accessor line
readonly get => _v;against:103:readonlynot in the alternation → 0 modifiers consumed.(?<returnType>[\w?.<>\[\],:]+)→ matchesreadonly.\s+(?<name>\w+)→ matchesget(name=get).\s*=>\s*→ matches=>.Match succeeds; symbol recorded as
property get. There is no containing-property gate, no "did we just see a{opening this member?" check, so the accessor line is taken to be a top-level property declaration all on its own.Why both effects share one fix
Adding
readonlyto the modifier slot in both:100and:103:— for the phantom
readonly get => _v;line,readonlyis now consumed by the modifier slot, then(?<returnType>\w+)must match a non-empty returnType followed by\s+(?<name>\w+)\s*=>\s*. The remaining text isget => _v;, which has noname \s*=>shape (there is no second identifier betweengetand=>). Match fails; no phantom.— for
public readonly int A => _v;,readonlyis now consumed by the modifier slot, leavingint A => _v;. returnType matchesint, name matchesA,=>matches. Match succeeds; property captured.Both effects fall out of the single one-token addition.
Suggested direction
SymbolExtractor.cs:100and:103to includereadonly. Also considerunsafe,extern, andfilefor completeness, since those are all valid property modifiers in current C# (e.g.file int X { get; }on a file-scoped struct,unsafe int* P { get; }in combination with C#: methods with pointer / function-pointer return types are dropped from the symbol index (int*,void**,delegate*<...>,int*[]) #234).:118(already includesstatic|virtual|override|abstract|sealed|new— addreadonly).readonly get/set => expr;lines don't drive the expression-bodied property regex on their own, as a defense-in-depth against future modifier slots creating similar phantoms: either require the returnType to be followed by an identifier distinct fromget/set/init, or skip the expression-bodied property regex when the preceding non-blank line ends in{.SymbolExtractorTests.csfixtures asserting:propertyrow each with the expected name.readonly get => _v;on one accessor does NOT produce aproperty get/property setphantom row.readonly+unsafe+static+ pointer return (depends on C#: methods with pointer / function-pointer return types are dropped from the symbol index (int*,void**,delegate*<...>,int*[]) #234) — but at minimumreadonlyalone.Cross-language note
The property-modifier-slot gap is C#-specific. Closest analogues:
readonly), C# 13 partial properties (public partial string Name { get; set; }) silently dropped —partialnot in property-regex modifier list #228 (partial), C#: plain fields (public int Count;,private readonly List<int> _items;,public static int GlobalCount;) are not captured as symbols — onlyconstandstatic readonlyare indexed #298 (plain fields). Three separate missing-token families on the same extractor row.@MainActor,nonisolated,dynamicbeforevar/let; not captured as properties in the Swift regex row set, but Swift's symbol extractor shape is different enough that the bug does not directly transfer.var/valwith modifiers (open,override,final); the Kotlin row set has its own slot, orthogonal to this bug.The phantom from accessor line effect does generalize: any language whose per-accessor body syntax (e.g. hypothetical
readonly get => x;) coincidentally matches the "expression-bodied property" shape would be susceptible. Today the direct impact is C# only.Scope
src/CodeIndex/Indexer/SymbolExtractor.cs:100, :103, :118— addreadonly(and plausiblyunsafe|extern|file) to modifier alternations.tests/CodeIndex.Tests/SymbolExtractorTests.cs— fixtures for the threereadonlyproperty forms and for the accessor-line phantom suppression.DEVELOPER_GUIDE.mdlanguage-pattern reference table — note that C# properties supportreadonlymodifier.Related
public partial string Name { get; set; }) silently dropped —partialnot in property-regex modifier list #228 — C# 13partialproperties dropped (same modifier-slot gap, different token). Landing both fixes in one patch is the natural scope.{on the next line (block style) are silently dropped — only same-line{ get; set; }is captured #229 — Allman-style property brace-on-next-line dropped (independent root cause, but combines with this issue because the phantomproperty getis produced on the accessor line of a property whose opening line was already dropped by Allman-style).ref/ref readonlyreturn types on methods and properties are silently dropped —public ref T Find(...)produces zero symbols #224 — C#ref/ref readonlyreturn types (different slot;refmodifier before returnType, notreadonlyin the modifier slot).int*,void**,delegate*<...>,int*[]) #234 — C# pointer / function-pointer return types (adjacent "char class / modifier slot too narrow" family).static abstract/abstract staticinterface operators (C# 11 generic math) are dropped from the symbol index #244 — C# 11static abstractoperators (same family — modifier slot omits valid modifiers).public int Count;,private readonly List<int> _items;,public static int GlobalCount;) are not captured as symbols — onlyconstandstatic readonlyare indexed #298 — C# plain fields (same extractor, adjacent gap)."""..."""raw strings (and other multi-line strings) becomes phantom symbols — 52 fakeclass Approws on cdidx itself, topmapentrypoint is a Lua fixture string #177 — string-literal boundary tracking (orthogonal but same extractor).Environment
/root/.local/bin/cdidx, trimmed release).cdidx languagesshowscsharpwithyes/yes.CLOUD_BOOTSTRAP_PROMPT.md.