Skip to content

C# nested new delegate and new enum (hiding base-class nested delegate / enum) are silently dropped — new missing from delegate-regex (:105) and enum-regex (:75) modifier lists #353

@Widthdom

Description

@Widthdom

Summary

C# allows a derived class to hide an inherited nested type using the new modifier: public new enum E { A }, public new delegate int D();, public new class C { }, etc. SymbolExtractor added new to the class regex at :81 and the struct regex at :78, so new class and new struct are captured. But new is missing from the delegate row at :105 (modifier list static|unsafe) and the enum row at :75 (modifier list file only). As a result, every new delegate and new enum declaration in a derived class is silently dropped from the symbol index, even though this shape is standard C# inheritance hiding and appears in any API that evolves nested-type contracts across a class hierarchy.

public class Base
{
    public interface IFoo { }
    public delegate int Handler();
    public enum Kind { A }
}

public class Derived : Base
{
    public new class NestedCls { }   // CAPTURED — class row has `new`
    public new struct NestedStr { }  // CAPTURED — struct row has `new`
    public new interface IFoo { }    // DROPPED — #302/#335 already scoped to cover this
    public new delegate int Handler();  // DROPPED — THIS issue
    public new enum Kind { A }          // DROPPED — THIS issue
}

definition Handler misses the Derived.Handler declaration entirely, outline Derived.cs under-reports nested members, and symbols --kind delegate / --kind enum with a path filter on the derived class shows zero rows for the new-hidden types.

This issue is distinct from:

Repro

CDIDX=/root/.local/bin/cdidx
mkdir -p /tmp/dogfood/cs-new-nested
cat > /tmp/dogfood/cs-new-nested/N.cs <<'EOF'
namespace CsNewNested;

public class Base
{
    public interface IFoo { }
    public delegate int Handler();
    public class NestedCls { }
    public struct NestedStr { }
    public enum NestedEnm { A }
}

public class Derived : Base
{
    // `new` hides base-class nested types
    public new interface IFoo { }         // DROPPED (#302/#335 scope)
    public new delegate int Handler();    // DROPPED (this issue)
    public new class NestedCls { }        // CAPTURED (class row has `new`)
    public new struct NestedStr { }       // CAPTURED (struct row has `new`)
    public new enum NestedEnm { A }       // DROPPED (this issue)
}
EOF
"$CDIDX" index /tmp/dogfood/cs-new-nested --rebuild
"$CDIDX" symbols --db /tmp/dogfood/cs-new-nested/.cdidx/codeindex.db

Observed:

class      Base                                     N.cs:3-10
class      Derived                                  N.cs:12-21
delegate   Handler                                  N.cs:6      ← base delegate
interface  IFoo                                     N.cs:5      ← base interface
class      NestedCls                                N.cs:7      ← base
class      NestedCls                                N.cs:17     ← `new` derived ✓
enum       NestedEnm                                N.cs:9      ← base
struct     NestedStr                                N.cs:8      ← base
struct     NestedStr                                N.cs:18     ← `new` derived ✓
namespace  CsNewNested                              N.cs:1
(10 symbols in 1 files)

Missing: Handler at N.cs:16 (public new delegate int Handler()), NestedEnm at N.cs:20 (public new enum NestedEnm { A }), plus IFoo at N.cs:15 (covered by #302/#335 — out of scope here).

For inheritance-hiding delegates: definition Handler returns only the Base.Handler declaration, so callers of the Derived.Handler (distinct delegate type in C#'s name-resolution model) cannot be located by callers Handler and inspect Handler fails to surface both rows.

For inheritance-hiding enums: same story for definition NestedEnm; outline Derived.cs misrepresents the derived class's nested-type surface.

Suspected root cause (from reading the source)

Row A — delegate at src/CodeIndex/Indexer/SymbolExtractor.cs:105:

new("delegate",  new Regex(
    @"^\s*(?:(?<visibility>public|private|protected\s+internal|private\s+protected|protected|internal)\s+)?"
  + @"(?:(?:static|unsafe)\s+)?"
  + @"delegate\s+\S+\s+(?<name>\w+)\s*[\(<]",
    RegexOptions.Compiled),
    BodyStyle.None, "visibility"),

Modifier alternation is static|unsafe, using ? (zero or one). Missing: new, file (#303 tracks this), partial (arguably — not legal on delegates per spec, but worth noting), readonly (not legal).

Walkthrough for public new delegate int Handler();:

  1. ^\s* eats leading whitespace.
  2. visibility matches public + trailing space.
  3. (?:(?:static|unsafe)\s+)? attempts to match new. new is not in the list → zero iterations.
  4. Cursor at new delegate int Handler();.
  5. delegate\s+ literal expects delegate — next non-space token is newfails.
  6. No backtrack options; the regex never matches the line.

Compare with the class row :81 modifier list static|partial|abstract|sealed|readonly|file|new|unsafe — includes new, which is how public new class NestedCls { } is captured. The delegate row was not updated alongside.

Row B — enum at src/CodeIndex/Indexer/SymbolExtractor.cs:75:

new("enum",      new Regex(
    @"^\s*(?:(?<visibility>public|private|protected\s+internal|private\s+protected|protected|internal)\s+)?"
  + @"(?:(?:file)\s+)*"
  + @"enum\s+(?<name>\w+)",
    RegexOptions.Compiled),
    BodyStyle.Brace, "visibility"),

Modifier alternation is file only. Missing: new (this issue), also unsafe (not legal per spec, but other type rows include it as defensive).

Walkthrough for public new enum NestedEnm { A }:

  1. visibility matches public.
  2. (?:(?:file)\s+)* tries new — not in list → zero iterations.
  3. Cursor at new enum NestedEnm { A }.
  4. enum\s+ literal expects enum — next is newfails.

No other C# pattern rescues the line:

  • Class row :81 requires (?:record\s+class\s+|record\s+|class\s+)enum doesn't match any of those.
  • Struct row :78 requires struct — fails.
  • Interface row :73 requires interface — fails.
  • Method / ctor / property / indexer / event / operator / conversion operator rows all require forms the enum declaration doesn't satisfy.

Net: silent drop. No warning, no phantom.

Suggested direction

(A) Add new to the delegate regex modifier alternation at :105:

// :105 — change
(?:(?:static|unsafe)\s+)?
// to
(?:(?:static|unsafe|new|file)\s+)*

Key details:

(B) Add new to the enum regex modifier alternation at :75:

// :75 — change
(?:(?:file)\s+)*
// to
(?:(?:file|new)\s+)*

The enum row's modifier slot is already *, so combinations are supported.

(C) Align interface regex :73 with the same editnew interface is already proposed in #302 / #335's suggested fixes. Landing (A), (B), and the new edit from #302 / #335 together is cleaner than three separate commits.

(D) Tests. Add fixtures to SymbolExtractorTests.cs covering:

  • public new delegate int Handler(); → captured as delegate row.
  • public new delegate T Gen<T>(T x); → captured.
  • public new unsafe delegate int* P(); (delegate row also intersects with C#: methods with pointer / function-pointer return types are dropped from the symbol index (int*, void**, delegate*<...>, int*[]) #234 for pointer return; assert the delegate row itself fires and routes to :105 rather than dropping).
  • public new enum Kind { A } → captured as enum row.
  • public new enum Kind : byte { A } (with explicit underlying type) → captured.
  • Regression: public delegate int Handler(); / file delegate int D(); / public enum Kind { A } / file enum E { A } still captured.

Why it matters

Cross-language note

  • C#-specific. The new member-hiding modifier is C#-specific grammar. Java uses @Override semantics; Kotlin, Swift, Rust, TypeScript handle nested type hiding through different (or no) mechanisms.
  • The fix is C#-scoped; no peer language rows are affected.

Scope

Related

Environment

  • cdidx: v1.10.0 (/root/.local/bin/cdidx).
  • Platform: linux-x64.
  • Fixture: /tmp/dogfood/cs-new-nested/N.cs (inline in Repro).
  • 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