Summary
The C# enum-member regex at SymbolExtractor.cs:127 has no context about whether the current line is inside an enum { } body. Any line with the shape ^\s{2,}[A-Z]\w*\s*=\s*\d... — an indented PascalCase identifier assigned a numeric value followed by comma — matches the enum-member pattern and is extracted as a phantom function symbol.
In practice, this fires on C# object initializer assignments like Age = 30, inside new Person { Name = "Alice", Age = 30 }. Since C# property names are PascalCase and numeric property values are common, the enum-member regex produces phantom symbols in any file that uses object initializers with numeric values.
Repro
CDIDX=/root/.local/bin/cdidx
mkdir -p /tmp/dogfood/cs-objinit-phantom
cat > /tmp/dogfood/cs-objinit-phantom/O.cs <<'EOF'
namespace CsObjInitPhantom;
public class Person
{
public string Name { get; set; } = "";
public int Age { get; set; }
public int Priority { get; set; }
}
public class Creator
{
public Person CreatePerson() => new Person
{
Name = "Alice",
Age = 30,
Priority = 1
};
public List<Person> CreateMany() => new()
{
new Person { Name = "Bob", Age = 25, Priority = 2 },
new Person
{
Name = "Carol",
Age = 45,
Priority = 3
}
};
}
EOF
"$CDIDX" index /tmp/dogfood/cs-objinit-phantom --rebuild
"$CDIDX" symbols --db /tmp/dogfood/cs-objinit-phantom/.cdidx/codeindex.db
Observed:
function Age O.cs:15 ← PHANTOM — object initializer assignment
function Priority O.cs:16 ← PHANTOM — object initializer assignment
function Age O.cs:25 ← PHANTOM — object initializer assignment
function Priority O.cs:26 ← PHANTOM — object initializer assignment
property Age O.cs:6 ← real property (correct)
property Priority O.cs:8 ← real property (correct)
...
Expected: Only the property entries at lines 6 and 8 should exist for Age and Priority. The object initializer assignments at lines 15, 16, 25, 26 should NOT produce symbols.
Note: Name = "Alice", does NOT phantom because the string value "Alice" doesn't match the enum-member value pattern alternatives (-?\d, 0x, [A-Z]\w*\|). Only numeric assignments fire.
Suspected root cause
src/CodeIndex/Indexer/SymbolExtractor.cs:127:
new("function", new Regex(@"^\s{2,}(?<name>[A-Z]\w*)\s*(?:=\s*(?:-?\d|0x|[A-Z]\w*(?:\s*\|))[^""']*)?,?\s*$", RegexOptions.Compiled), BodyStyle.None),
Walkthrough for Age = 30,:
^\s{2,} → 8 spaces ✓
(?<name>[A-Z]\w*) → Age (starts with uppercase A) ✓
\s* → space ✓
(?:=\s*(?:-?\d|0x|...)[^"']*)? → = 30 matches: =\s* then -?\d matches 3, then [^"']* consumes 0 ✓
,? → , ✓
\s*$ → end of line ✓
The pattern cannot distinguish between:
- Enum member:
Red = 0, (inside enum Color { })
- Object initializer:
Age = 30, (inside new Person { })
Both have identical syntactic shape: indented PascalCase name, =, numeric value, trailing comma.
Suggested direction
The fundamental issue is that the enum-member regex has no context about the enclosing construct. Two approaches:
A. Require SCREAMING_SNAKE_CASE for enum members
Tighten the name group to [A-Z][A-Z0-9_]* instead of [A-Z]\w*. This would match RED, MAX_VALUE, HTTP_OK but NOT Age, Priority, Name. However, C# convention for enum members is PascalCase (Monday, HttpStatusCode.Ok), not SCREAMING_SNAKE — so this would break capture of standard C# enums. Not recommended for C#.
B. Add a negative lookahead for common object-initializer shapes
Require that the value is NOT just a bare decimal number when the name looks like a PascalCase property (mixed case):
// Only accept as enum member if:
// (a) name is ALL_CAPS (high confidence enum member), OR
// (b) name is PascalCase AND the value is not a bare number (distinguishes from object initializer)
This is fragile. A cleaner approach:
C. Track whether we're inside an enum body
Add an inEnumBody flag to the extraction loop. When the enum regex at :74 matches and has BodyStyle.Brace, track that subsequent lines until the matching } are inside an enum. Only apply the enum-member regex when inEnumBody is true. This is the most robust fix and eliminates all non-enum false positives.
D. Re-anchor by requiring AssignContainers context
Post-process enum-member symbols and remove any whose enclosing container is not an enum. AssignContainers (:488) already runs after extraction and assigns parent symbols. A sweep after assignment could remove function symbols whose parent is a class/struct/etc.
Cross-language note
Scope
src/CodeIndex/Indexer/SymbolExtractor.cs:127 — add context restriction so the enum-member regex only fires inside enum bodies (option C or D).
tests/CodeIndex.Tests/SymbolExtractorTests.cs — add fixtures for object initializer with numeric values (should NOT phantom), enum member (should still capture), nested object initializer.
Related
Environment
- cdidx v1.10.0 (installed via
install.sh to /root/.local/bin/cdidx).
- Platform: linux-x64 container.
- Filed from a cloud Claude Code session per
CLOUD_BOOTSTRAP_PROMPT.md.
Summary
The C# enum-member regex at
SymbolExtractor.cs:127has no context about whether the current line is inside anenum { }body. Any line with the shape^\s{2,}[A-Z]\w*\s*=\s*\d...— an indented PascalCase identifier assigned a numeric value followed by comma — matches the enum-member pattern and is extracted as a phantomfunctionsymbol.In practice, this fires on C# object initializer assignments like
Age = 30,insidenew Person { Name = "Alice", Age = 30 }. Since C# property names are PascalCase and numeric property values are common, the enum-member regex produces phantom symbols in any file that uses object initializers with numeric values.Repro
Observed:
Expected: Only the
propertyentries at lines 6 and 8 should exist forAgeandPriority. The object initializer assignments at lines 15, 16, 25, 26 should NOT produce symbols.Note:
Name = "Alice",does NOT phantom because the string value"Alice"doesn't match the enum-member value pattern alternatives (-?\d,0x,[A-Z]\w*\|). Only numeric assignments fire.Suspected root cause
src/CodeIndex/Indexer/SymbolExtractor.cs:127:Walkthrough for
Age = 30,:^\s{2,}→ 8 spaces ✓(?<name>[A-Z]\w*)→Age(starts with uppercaseA) ✓\s*→ space ✓(?:=\s*(?:-?\d|0x|...)[^"']*)?→= 30matches:=\s*then-?\dmatches3, then[^"']*consumes0✓,?→,✓\s*$→ end of line ✓The pattern cannot distinguish between:
Red = 0,(insideenum Color { })Age = 30,(insidenew Person { })Both have identical syntactic shape: indented PascalCase name,
=, numeric value, trailing comma.Suggested direction
The fundamental issue is that the enum-member regex has no context about the enclosing construct. Two approaches:
A. Require SCREAMING_SNAKE_CASE for enum members
Tighten the name group to
[A-Z][A-Z0-9_]*instead of[A-Z]\w*. This would matchRED,MAX_VALUE,HTTP_OKbut NOTAge,Priority,Name. However, C# convention for enum members is PascalCase (Monday,HttpStatusCode.Ok), not SCREAMING_SNAKE — so this would break capture of standard C# enums. Not recommended for C#.B. Add a negative lookahead for common object-initializer shapes
Require that the value is NOT just a bare decimal number when the name looks like a PascalCase property (mixed case):
This is fragile. A cleaner approach:
C. Track whether we're inside an enum body
Add an
inEnumBodyflag to the extraction loop. When the enum regex at:74matches and hasBodyStyle.Brace, track that subsequent lines until the matching}are inside an enum. Only apply the enum-member regex wheninEnumBodyis true. This is the most robust fix and eliminates all non-enum false positives.D. Re-anchor by requiring
AssignContainerscontextPost-process enum-member symbols and remove any whose enclosing container is not an
enum.AssignContainers(:488) already runs after extraction and assigns parent symbols. A sweep after assignment could removefunctionsymbols whose parent is aclass/struct/etc.Cross-language note
enummember pattern over-matches: anyUPPERCASE(...);call statement becomes a phantomfunctiondefinition — fires outside enum bodies and inside text blocks #292) but withSCREAMING_SNAKE_CASEconvention — Java's problem manifests on uppercase call statements, not PascalCase properties.Monday,Red), same as properties, making the false-positive surface much larger.Scope
src/CodeIndex/Indexer/SymbolExtractor.cs:127— add context restriction so the enum-member regex only fires inside enum bodies (option C or D).tests/CodeIndex.Tests/SymbolExtractorTests.cs— add fixtures for object initializer with numeric values (should NOT phantom), enum member (should still capture), nested object initializer.Related
enummember pattern over-matches: anyUPPERCASE(...);call statement becomes a phantomfunctiondefinition — fires outside enum bodies and inside text blocks #292 — Java enum-member regex over-matches call statements. Same family (enum-member pattern fires outside enum context), different language and trigger shape.= K.Foo,= (int)1.5,= (1 + 2),= 'A',= System.Int32.MaxValue) are silently dropped — value-shape alternation accepts only\d/0x/[A-Z]\w*\|#357 — C# enum members with complex values silently dropped (opposite direction: too-restrictive value matching).:127regex requires\s{2,}but a tab is only 1 character #364 — C# enum members with tab indent dropped (indent-width issue).propertysymbols (incl. constant patterns producing nonsensical names like0) #208 — Switch expression arm phantoms. Same "pattern matches non-declaration code inside blocks" family.Environment
install.shto/root/.local/bin/cdidx).CLOUD_BOOTSTRAP_PROMPT.md.