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
C#: interpolated strings $"... {Foo()} ..." hide embedded call sites from the reference index; verbatim strings @"..." confuse the extractor on line continuations #264
ReferenceExtractor.StringLiteralRegex treats every "..." span as an opaque literal and replaces it with "" before the reference regex runs. This is correct for plain string literals, but C# supports two variants that contain executable code inside the quotes:
Interpolated strings$"Hello {Foo()}" — the {Foo()} is compiled as a real call; cdidx erases it along with the surrounding literal, so Foo is never registered as a reference.
Verbatim strings@"multi\nline" and verbatim interpolated$@"..." — these strings can span multiple source lines. StringLiteralRegex is line-scoped (no RegexOptions.Singleline, and the constructor at ReferenceExtractor.cs:71-73 uses [^"\\\\] which doesn't match the verbatim "" escape either), so the continuation lines look like plain code. Any identifier-with-paren inside the verbatim string body gets incorrectly captured as a call reference.
Both are silent. Interpolated-call sites are lost; verbatim-string continuation lines inject phantom references.
Repro
CDIDX=/root/.local/bin/cdidx
mkdir -p /tmp/dogfood/cs-interp
cat > /tmp/dogfood/cs-interp/I.cs <<'EOF'namespace Demo;public class Helper{ public static string GetName() => "bob"; public static int GetAge() => 42; public static string Format(string s) => s;}public class Caller{ public void Work() { var s1 = $"Hello {Helper.GetName()}"; // interpolated — dropped var s2 = $"Age: {Helper.GetAge()} years"; // dropped var s3 = $"Nested {Helper.Format(Helper.GetName())}"; // dropped var s4 = $@"Multiline {Helper.GetName()} text"; // phantom capture on line 2 var s5 = Helper.GetName(); // plain — captured }}EOF"$CDIDX" index /tmp/dogfood/cs-interp --rebuild
"$CDIDX" references GetName --db /tmp/dogfood/cs-interp/.cdidx/codeindex.db --exact
"$CDIDX" references GetAge --db /tmp/dogfood/cs-interp/.cdidx/codeindex.db --exact
"$CDIDX" references Format --db /tmp/dogfood/cs-interp/.cdidx/codeindex.db --exact
Observed:
--- references GetName ---
call GetName I.cs:15:14 in Work ← continuation of verbatim string — WRONG capture
line {Helper.GetName()} text";
call GetName I.cs:17:25 in Work ← plain call
(2 references in 1 files)
← real calls on lines 12, 13, 14 are missing
--- references GetAge ---
No references found. ← `GetAge` in `$"Age: {Helper.GetAge()} years"` dropped
--- references Format ---
No references found. ← `Format` in `$"Nested {Helper.Format(...)}"` dropped
Expected: at least one reference row for each of GetName (lines 12, 14, 15, 17 — four real call sites), GetAge (line 13), and Format (line 14). The line-15 phantom capture should not be present.
No interpolation-hole skip. The regex is designed for plain literals. C# interpolated strings $"text {expr} text" hold real expressions inside {...} at the source level; erasing the whole span loses those expressions. Languages with the same pattern: JavaScript/TypeScript template literals (`${expr}`) — the backtick branch of this same regex has the same blind spot.
No verbatim-string awareness. C# @"..." uses "" (doubled quote) as the escape for ", not \". The current class [^"\\\\] accepts any char except " or \, and the escape alternative \\\\. only handles \\X. For @"He said ""Hi"" and left", the regex stops at the first internal " on the character before Hi, thinking the literal has ended, and then re-enters the next literal at the start of Hi". This doesn't affect all verbatim strings but is wrong in general.
Line-scoped application.PrepareLine() is called per source line. A verbatim string that spans multiple source lines:
vars=$@"startmiddle {Foo()} moreend";
— only the first line has an opening ". Lines 2 and 3 are processed as plain code. middle {Foo()} more is scanned and Foo( matches CallRegex, yielding a phantom call reference that is actually inside a string. Line 3 has a dangling closing " that may then confuse the next line's literal tracking (it doesn't cross lines, but the leading " on line 3 looks like the start of a new empty string, so end before it is left as code and produces nothing obviously wrong — but the overall situation is fragile).
Language-global. The same regex is applied to every language. Python f-strings f"text {expr}", JavaScript / TypeScript template literals `text ${expr}`, Kotlin "text $variable ${expr()}", and Swift "text \(expr)" all have the same "real code inside quotes" issue.
Suggested direction
Two complementary fixes:
1. Interpolation-hole preservation.
Add a language-aware pre-pass that, before the generic literal erasure, scans for interpolated-string spans and rewrites them to keep the hole contents while erasing the literal framing. For C#:
// Matches $"..." (non-verbatim) while preserving `{...}` hole contents.privatestaticreadonlyRegexCsInterpolatedStringRegex=new(@"\$""(?<body>(?:[^""{}\\]|\\.|\{\{|\}\}|\{[^{}]*\})*)""",RegexOptions.Compiled);preparedLine=CsInterpolatedStringRegex.Replace(preparedLine, m =>{// Replace literal text with spaces (preserve column offsets), keep hole contents.varbody=m.Groups["body"].Value;// Scan body, replace non-hole chars with spaces; keep '{...}' contents intact.
...});
For JS/TS template literals, mirror with `...${expr}...`. For Python f-strings, the same idea applies to f"..." and f'...'.
2. Verbatim / multi-line string masking.
Add a stateful pre-pass (or a single whole-file pass before line splitting) that tracks "inside a verbatim string" state across lines. When a verbatim opens on line N and closes on line M > N, mark lines N+1..M as "inside string" and replace the entire content of those lines with spaces before running the reference regexes. C# verbatim literals: @"...", $@"...", @$"...", and the C# 11 raw-string literals """...""" (which may also span multiple lines, with their own escaping rules).
A whole-file scan once per file is O(N) and avoids the per-line regex pathology. It also lets the interpolation-hole preservation work uniformly for single-line and multi-line interpolated strings.
Why it matters
Interpolated strings are ubiquitous in modern C#. Logging (_logger.LogInformation($"Started {service.Name}")), error messages (throw new Exception($"Missing {config.Key}")), SQL building ($"SELECT * FROM {table.Name}"), format strings everywhere. All of these service.Name, config.Key, table.Name calls are invisible to the reference index. For many service classes, Name/Id property accesses via interpolation are the majority of real usage.
Phantom captures from verbatim strings produce false positives in references, callers, and impact. A multi-line SQL @"SELECT foo FROM bar WHERE baz()" captured across lines yields a phantom baz() call. These false positives are harder to spot than simple missing data — the tool confidently points at an in-string location as a call site.
unused analysis. Properties/methods referenced only via interpolation show up as "zero callers" → false-positive unused.
Cross-language surface. JS/TS template literals, Python f-strings, Kotlin string interpolation, Swift \(expr) all exhibit the same "dropped call in interpolated string" pattern once fixed for C#.
Cross-language note
JavaScript/TypeScript`Hello ${getName()}` — the existing backtick branch of StringLiteralRegex already erases the whole thing, so getName is lost. Template literals are extremely common in modern JS/TS.
Python f-stringsf"hello {get_name()}" — the "..." branch of the regex matches, erasing the interpolation. Standard Python 3.6+ syntax, ubiquitous.
Kotlin"Hello ${getName()} age=${age}" — same pattern as C# interpolation.
Scalas"Hello ${getName()}" — same pattern.
Swift"Hello \(getName())" — Swift uses \(expr) as the interpolation form.
Multi-line — C# @"...", Kotlin """...""", Python triple-quoted strings """...""", JS template literals `...\n...`, Scala """...""" — all can span lines; all suffer from the line-scoped erasure gap.
The two fixes above (interpolation-hole preservation, multi-line literal masking) together address the majority of this cross-language surface.
Summary
ReferenceExtractor.StringLiteralRegextreats every"..."span as an opaque literal and replaces it with""before the reference regex runs. This is correct for plain string literals, but C# supports two variants that contain executable code inside the quotes:$"Hello {Foo()}"— the{Foo()}is compiled as a real call; cdidx erases it along with the surrounding literal, soFoois never registered as a reference.@"multi\nline"and verbatim interpolated$@"..."— these strings can span multiple source lines.StringLiteralRegexis line-scoped (noRegexOptions.Singleline, and the constructor atReferenceExtractor.cs:71-73uses[^"\\\\]which doesn't match the verbatim""escape either), so the continuation lines look like plain code. Any identifier-with-paren inside the verbatim string body gets incorrectly captured as a call reference.Both are silent. Interpolated-call sites are lost; verbatim-string continuation lines inject phantom references.
Repro
Observed:
Expected: at least one reference row for each of
GetName(lines 12, 14, 15, 17 — four real call sites),GetAge(line 13), andFormat(line 14). The line-15 phantom capture should not be present.Suspected root cause (from reading the source)
src/CodeIndex/Indexer/ReferenceExtractor.cs:71-73:Issues:
No interpolation-hole skip. The regex is designed for plain literals. C# interpolated strings
$"text {expr} text"hold real expressions inside{...}at the source level; erasing the whole span loses those expressions. Languages with the same pattern: JavaScript/TypeScript template literals (`${expr}`) — the backtick branch of this same regex has the same blind spot.No verbatim-string awareness. C#
@"..."uses""(doubled quote) as the escape for", not\". The current class[^"\\\\]accepts any char except"or\, and the escape alternative\\\\.only handles\\X. For@"He said ""Hi"" and left", the regex stops at the first internal"on the character beforeHi, thinking the literal has ended, and then re-enters the next literal at the start ofHi". This doesn't affect all verbatim strings but is wrong in general.Line-scoped application.
PrepareLine()is called per source line. A verbatim string that spans multiple source lines:— only the first line has an opening
". Lines 2 and 3 are processed as plain code.middle {Foo()} moreis scanned andFoo(matchesCallRegex, yielding a phantom call reference that is actually inside a string. Line 3 has a dangling closing"that may then confuse the next line's literal tracking (it doesn't cross lines, but the leading"on line 3 looks like the start of a new empty string, soendbefore it is left as code and produces nothing obviously wrong — but the overall situation is fragile).Language-global. The same regex is applied to every language. Python f-strings
f"text {expr}", JavaScript / TypeScript template literals`text ${expr}`, Kotlin"text $variable ${expr()}", and Swift"text \(expr)"all have the same "real code inside quotes" issue.Suggested direction
Two complementary fixes:
1. Interpolation-hole preservation.
Add a language-aware pre-pass that, before the generic literal erasure, scans for interpolated-string spans and rewrites them to keep the hole contents while erasing the literal framing. For C#:
For JS/TS template literals, mirror with
`...${expr}...`. For Python f-strings, the same idea applies tof"..."andf'...'.2. Verbatim / multi-line string masking.
Add a stateful pre-pass (or a single whole-file pass before line splitting) that tracks "inside a verbatim string" state across lines. When a verbatim opens on line N and closes on line M > N, mark lines N+1..M as "inside string" and replace the entire content of those lines with spaces before running the reference regexes. C# verbatim literals:
@"...",$@"...",@$"...", and the C# 11 raw-string literals"""..."""(which may also span multiple lines, with their own escaping rules).A whole-file scan once per file is O(N) and avoids the per-line regex pathology. It also lets the interpolation-hole preservation work uniformly for single-line and multi-line interpolated strings.
Why it matters
_logger.LogInformation($"Started {service.Name}")), error messages (throw new Exception($"Missing {config.Key}")), SQL building ($"SELECT * FROM {table.Name}"), format strings everywhere. All of theseservice.Name,config.Key,table.Namecalls are invisible to the reference index. For many service classes,Name/Idproperty accesses via interpolation are the majority of real usage.references,callers, andimpact. A multi-line SQL@"SELECT foo FROM bar WHERE baz()"captured across lines yields a phantombaz()call. These false positives are harder to spot than simple missing data — the tool confidently points at an in-string location as a call site.unusedanalysis. Properties/methods referenced only via interpolation show up as "zero callers" → false-positive unused.\(expr)all exhibit the same "dropped call in interpolated string" pattern once fixed for C#.Cross-language note
`Hello ${getName()}`— the existing backtick branch ofStringLiteralRegexalready erases the whole thing, sogetNameis lost. Template literals are extremely common in modern JS/TS.f"hello {get_name()}"— the"..."branch of the regex matches, erasing the interpolation. Standard Python 3.6+ syntax, ubiquitous."Hello ${getName()} age=${age}"— same pattern as C# interpolation.s"Hello ${getName()}"— same pattern."Hello \(getName())"— Swift uses\(expr)as the interpolation form.@"...", Kotlin"""...""", Python triple-quoted strings"""...""", JS template literals`...\n...`, Scala"""..."""— all can span lines; all suffer from the line-scoped erasure gap.The two fixes above (interpolation-hole preservation, multi-line literal masking) together address the majority of this cross-language surface.
Scope
src/CodeIndex/Indexer/ReferenceExtractor.cs— language-aware interpolation-hole preservation; multi-line verbatim / triple-quoted / backtick-template masking.tests/CodeIndex.Tests/ReferenceExtractorTests.cs— fixtures for C#$"...",$@"...","""..."""(C# 11 raw), JS/TS template literals, Python f-strings, Kotlin interpolation, multi-line verbatim.CLAUDE.mddesign-decisions — note the stateful pre-pass (one-per-file) to keep per-line reference scanning correct.Related
IgnoredCallNameslanguage-global (same extractor architecture; orthogonal).Task<Result<A, B>>,Dictionary<K, V>) are silently dropped — idiomatic .NET formatting is effectively unindexed #222 — C# return-type parsing with spaces (extractor family, symbol-side).nameof(X.Y)/typeof(T)/default(T)arguments are silently dropped from the reference index #253, C#/Java: type-position identifiers (base classes, parameter/field/return types, generic constraints,is/aspatterns, attributes) never registered as references #256, C#: constructor chain calls: this(...)and: base(...)are never recorded as references —callerson a constructor misses every subclass and every overload #257, Rust: macro invocationsname!(...)/name![...]/name!{...}are never captured as references —println!,vec!,format!,dbg!, usermacro_rules!all silent #258, Python: bare decorator usages@decorator(no call parens) are not captured as references —@staticmethod,@property,@pytest.fixture, every bare-dec idiom silent #259, Ruby: no-parens method calls (greet "bob",puts "hi",raise ArgumentError, "bad") are never captured as references — the most idiomatic Ruby call form is invisible to the graph #260 — the no-parens reference-extraction family (same extractor; this issue is the string-literal counterpart).new Dict<K, List<V>>()/Foo<Bar<int>>()are silently dropped from the reference index — the>>tail breaks the generic-arg regex #263 — nested-generic call sites dropped (sibling regex over-narrowing on the same extractor).Environment
install.shto/root/.local/bin/cdidx).CLOUD_BOOTSTRAP_PROMPT.md.