Skip to content

C# enum member regex over-matches object initializer assignments (Age = 30, inside new Person { } becomes phantom function) #374

Description

@Widthdom

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,:

  1. ^\s{2,} → 8 spaces ✓
  2. (?<name>[A-Z]\w*)Age (starts with uppercase A) ✓
  3. \s* → space ✓
  4. (?:=\s*(?:-?\d|0x|...)[^"']*)?= 30 matches: =\s* then -?\d matches 3, then [^"']* consumes 0
  5. ,?,
  6. \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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions