Summary
C# allows @-prefixed identifiers to use reserved keywords as names (e.g. @class, @int, @return, @void). This is a standard feature used in interop scenarios, generated code (EF Core DbContext generators, source generators), and library code wrapping keyword-named constructs from other languages. All C# symbol regexes in SymbolExtractor.cs use (?<name>\w+) for the name group, which does NOT include @ (a \w character matches letters, digits, and underscore, but not @). Any member declaration with an @-prefixed name is silently dropped.
Unicode identifiers (ギリシャ文字, 日本語, Russian) work correctly because they ARE matched by \w in .NET's regex engine, which is Unicode-aware.
Repro
CDIDX=/root/.local/bin/cdidx
mkdir -p /tmp/dogfood/cs-verbatim-ident
cat > /tmp/dogfood/cs-verbatim-ident/V.cs <<'EOF'
namespace CsVerbatimIdent;
public class Svc
{
// @-prefixed property names — DROPPED
public int @int { get; set; }
public string @class { get; set; } = "";
// @-prefixed method — DROPPED
public void @return() { }
// Method with @-prefixed parameters — captured (params don't affect method name regex)
public void Use(int @int, string @class) { }
// Unicode identifier — captured (\w matches Unicode letters)
public int 数量 { get; set; }
public void МетодРусский() { }
}
EOF
"$CDIDX" index /tmp/dogfood/cs-verbatim-ident --rebuild
"$CDIDX" symbols --db /tmp/dogfood/cs-verbatim-ident/.cdidx/codeindex.db
Expected: @int, @class, @return, Use, 数量, МетодРусский + Svc + namespace = 8 symbols.
Actual:
class Svc V.cs:3-17
function Use V.cs:12
function МетодРусский V.cs:16
property 数量 V.cs:15
namespace CsVerbatimIdent V.cs:1
Missing: @int, @class (properties at lines 6-7), @return (method at line 10). All symbols declared with @-prefixed names are silently dropped.
Suspected root cause
src/CodeIndex/Indexer/SymbolExtractor.cs:94, 100, 103, 116, 118, 127 — every C# name group uses (?<name>\w+):
// :94 — method
...\s+(?<name>\w+)\s*(?:<[^>]+>\s*)?\(
// :100 — property with get/set/init
...\s+(?<name>\w+)\s*\{\s*(?:get|set|init)
// :103 — expression-bodied property
...\s+(?<name>\w+)\s*=>\s*
// :107 — event
...event\s+\S+\s+(?<name>\w+)
// :127 — enum member
^\s{2,}(?<name>[A-Z]\w*)\s*...
\w in .NET regex (default mode) matches [A-Za-z0-9_] plus Unicode letter categories (\p{L}, \p{Mn}, \p{Nd}, \p{Pc}). It does NOT match @.
For public int @int { get; set; }:
- Property regex at
:100 tries to match. Visibility public, modifier empty, returnType int. Then \s+(?<name>\w+) — \s+ matches space. \w+ tries to match @int — but @ is not a \w character. \w+ requires at least one match, so it matches nothing at @ and immediately fails.
- Backtrack the returnType:
in (partial), then \s+ needs whitespace after in — but next char is t. Fails.
- No other alternative exists.
Suggested fix
Change the name group to accept an optional leading @:
Applied uniformly to rows :94, :97, :100, :103, :105, :107, :116, :118, :120, :122, :127. Each is a single-character addition.
Decision point: should the captured name include or exclude the @ prefix? The C# semantics: @class and class are different identifiers at the declaration site, but equivalent at the usage site (both refer to the same symbol). Two options:
- Include
@ in the captured name — preserves the exact declaration. definition @class works. definition class falls back to the keyword-named type if any.
- Strip
@ after capture — normalize to the identifier name. definition class works for both @class and plain class (but class is reserved so can only be a keyword-identifier in practice). Requires post-capture trim.
Option 2 better matches the semantic model (the identifier is "class", @ is syntactic only). Option 1 is simpler and matches the source text exactly.
Either way, the reference extractor's CallRegex ((?<name>[A-Za-z_]\w*)) already doesn't handle @, so a full fix requires updating ReferenceExtractor.cs too — otherwise @foo() calls wouldn't reference the symbol.
Why it matters
- Generated code frequently uses
@-prefixes. EF Core DbContext uses @if, @else columns in databases keyword-tolerant; source generators generate methods with @-prefixed names when the input is a keyword. ASP.NET action parameters named @from, @to are common in route binding.
- Interop with other languages (VB.NET, F#, Java via language interop, Python via Python.NET) may produce C# wrappers where the original identifier collides with a C# keyword, requiring
@ prefix.
- Silent drop. The symbol is invisible to
definition, symbols, outline, inspect, callers, etc.
- Unicode identifiers work correctly — demonstrating that the regex is Unicode-aware. The
@ gap is specifically about the ASCII verbatim-identifier prefix.
Cross-language note
- C#-specific. The
@ verbatim identifier prefix is C# syntax (Roslyn ecosystem).
- VB.NET uses
[keyword] brackets for the same purpose — not applicable here.
- F# uses
`keyword` backticks — not applicable.
- Kotlin uses backticks too — the Kotlin extractor should be checked separately.
Scope
src/CodeIndex/Indexer/SymbolExtractor.cs:94, 97, 100, 103, 105, 107, 116, 118, 120, 122, 127 — add @? before \w+ in the name group of each C# row.
src/CodeIndex/Indexer/ReferenceExtractor.cs:75-76 — ConstructorCallRegex and CallRegex name groups should also accept @ prefix so calls to @foo() resolve.
tests/CodeIndex.Tests/SymbolExtractorTests.cs — fixtures for @int, @class, @return, @void as method/property/field names.
tests/CodeIndex.Tests/ReferenceExtractorTests.cs — fixtures for calls to @foo().
Related
Environment
- cdidx: v1.10.0 (
/root/.local/bin/cdidx)
- Platform: Linux 4.4.0
- Fixture inline in Repro section.
Summary
C# allows
@-prefixed identifiers to use reserved keywords as names (e.g.@class,@int,@return,@void). This is a standard feature used in interop scenarios, generated code (EF Core DbContext generators, source generators), and library code wrapping keyword-named constructs from other languages. All C# symbol regexes inSymbolExtractor.csuse(?<name>\w+)for the name group, which does NOT include@(a\wcharacter matches letters, digits, and underscore, but not@). Any member declaration with an@-prefixed name is silently dropped.Unicode identifiers (ギリシャ文字, 日本語, Russian) work correctly because they ARE matched by
\win .NET's regex engine, which is Unicode-aware.Repro
Expected:
@int,@class,@return,Use,数量,МетодРусский+Svc+ namespace = 8 symbols.Actual:
Missing:
@int,@class(properties at lines 6-7),@return(method at line 10). All symbols declared with@-prefixed names are silently dropped.Suspected root cause
src/CodeIndex/Indexer/SymbolExtractor.cs:94, 100, 103, 116, 118, 127— every C# name group uses(?<name>\w+):\win .NET regex (default mode) matches[A-Za-z0-9_]plus Unicode letter categories (\p{L},\p{Mn},\p{Nd},\p{Pc}). It does NOT match@.For
public int @int { get; set; }::100tries to match. Visibilitypublic, modifier empty, returnTypeint. Then\s+(?<name>\w+)—\s+matches space.\w+tries to match@int— but@is not a\wcharacter.\w+requires at least one match, so it matches nothing at@and immediately fails.in(partial), then\s+needs whitespace afterin— but next char ist. Fails.Suggested fix
Change the name group to accept an optional leading
@:Applied uniformly to rows
:94,:97,:100,:103,:105,:107,:116,:118,:120,:122,:127. Each is a single-character addition.Decision point: should the captured name include or exclude the
@prefix? The C# semantics:@classandclassare different identifiers at the declaration site, but equivalent at the usage site (both refer to the same symbol). Two options:@in the captured name — preserves the exact declaration.definition @classworks.definition classfalls back to the keyword-named type if any.@after capture — normalize to the identifier name.definition classworks for both@classand plainclass(butclassis reserved so can only be a keyword-identifier in practice). Requires post-capture trim.Option 2 better matches the semantic model (the identifier is "class",
@is syntactic only). Option 1 is simpler and matches the source text exactly.Either way, the reference extractor's
CallRegex((?<name>[A-Za-z_]\w*)) already doesn't handle@, so a full fix requires updatingReferenceExtractor.cstoo — otherwise@foo()calls wouldn't reference the symbol.Why it matters
@-prefixes. EF Core DbContext uses@if,@elsecolumns in databases keyword-tolerant; source generators generate methods with@-prefixed names when the input is a keyword. ASP.NET action parameters named@from,@toare common in route binding.@prefix.definition,symbols,outline,inspect,callers, etc.@gap is specifically about the ASCII verbatim-identifier prefix.Cross-language note
@verbatim identifier prefix is C# syntax (Roslyn ecosystem).[keyword]brackets for the same purpose — not applicable here.`keyword`backticks — not applicable.Scope
src/CodeIndex/Indexer/SymbolExtractor.cs:94, 97, 100, 103, 105, 107, 116, 118, 120, 122, 127— add@?before\w+in the name group of each C# row.src/CodeIndex/Indexer/ReferenceExtractor.cs:75-76—ConstructorCallRegexandCallRegexname groups should also accept@prefix so calls to@foo()resolve.tests/CodeIndex.Tests/SymbolExtractorTests.cs— fixtures for@int,@class,@return,@voidas method/property/field names.tests/CodeIndex.Tests/ReferenceExtractorTests.cs— fixtures for calls to@foo().Related
function script:Name/global:/local:/private:scope prefixes make the extractor capture the scope keyword as the symbol name and lose the real name #308 — PowerShell scope-prefix name capture issue. Analogous pattern (prefix character not in\w), different language."""..."""raw strings (and other multi-line strings) becomes phantom symbols — 52 fakeclass Approws on cdidx itself, topmapentrypoint is a Lua fixture string #177 — raw string phantoms. Same per-line regex architecture.Environment
/root/.local/bin/cdidx)