Skip to content

C# events with abstract / virtual / override / sealed / new modifiers are silently dropped from the symbol index #334

@Widthdom

Description

@Widthdom

Summary

The C# event regex only accepts visibility? and static? as modifiers before the event keyword. In real C# codebases, events very commonly carry other modifiers — abstract in abstract-class contracts, virtual / override / sealed in inheritance chains, new for hiding, and static abstract / static virtual in C# 11 interface contracts. All of these silently drop the event from the symbol index. Only plain public event … and protected event … (visibility-only) and static event … survive. This is a very common real-world pattern — any abstract base class or interface event contract is invisible to symbols, definition, outline, references, etc.

Repro

CDIDX=/root/.local/bin/cdidx
mkdir -p /tmp/dogfood/cs-event-modifiers && cat > /tmp/dogfood/cs-event-modifiers/E.cs <<'EOF'
using System;
namespace EventMods;

public abstract class Base
{
    public abstract event EventHandler Ping;        // DROPPED
    public virtual event EventHandler Ring;         // DROPPED
    public new event EventHandler Hide;             // DROPPED
    protected event EventHandler Peek;              // captured
    public event EventHandler Plain;                // captured
}

public sealed class Derived : Base
{
    public override event EventHandler Ping;        // DROPPED
    public sealed override event EventHandler Ring; // DROPPED
}

public interface IBus
{
    event EventHandler Regular;                              // captured
    static abstract event EventHandler StaticAbs;            // DROPPED
    static virtual event EventHandler StaticVirt { add {} remove {} }  // DROPPED
}
EOF
"$CDIDX" index /tmp/dogfood/cs-event-modifiers
"$CDIDX" symbols --db /tmp/dogfood/cs-event-modifiers/.cdidx/codeindex.db --kind event

Expected: 9 event rows. Observed: 3 (Peek, Plain, Regular). The six events with abstract, virtual, override, sealed override, new, static abstract, static virtual modifiers are silently dropped.

Suspected root cause

src/CodeIndex/Indexer/SymbolExtractor.cs:107 — the C# event regex:

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

The pre-event modifier slot consumes only an optional visibility and an optional static. Anything else — abstract, virtual, override, sealed, new, extern, unsafe, file — blocks the match because the regex requires the event keyword immediately after static?. Compare with the method regex at :94 whose modifier slot includes virtual|override|sealed|abstract|async|extern|new|unsafe|partial — the event regex is the only row in the C# set that is this restrictive.

Suggested direction

Replace the restrictive (?:(?:static)\s+)? slot with the same repeatable modifier set used by the method regex, minus the clearly-irrelevant ones:

@"^\s*(?:(?<visibility>public|private|protected\s+internal|private\s+protected|protected|internal)\s+)?(?:(?:static|abstract|virtual|override|sealed|new|extern|unsafe|file)\s+)*event\s+\S+\s+(?<name>\w+)"

Key changes:

  • (?:(?:static)\s+)?(?:(?:static|abstract|virtual|override|sealed|new|extern|unsafe|file)\s+)*
  • * instead of ? so combinations like sealed override and static abstract are both accepted.

Consider whether required should be in the list too — required is not valid on an event per the C# spec (only on fields/properties), so it can be omitted. Likewise readonly is not valid on events, so omit.

An additional tightening worth considering: after this fix lands, consider making the regex verify that the keyword is exactly event (it already does) and that the captured name isn't a C# contextual keyword (event, add, remove) to avoid any future phantom risk from event-accessor block bodies on the same line.

Cross-language note

Event declarations are idiosyncratic to C# / VB.NET. Java has no direct equivalent; TypeScript / Kotlin / Swift all model the same concept via generic listener types on regular properties, so their patterns are unaffected.

Scope

  • Affected: src/CodeIndex/Indexer/SymbolExtractor.cs:107. Downstream: symbols, definition, outline, references, callers, inspect, map.
  • Particularly impactful for codebases with abstract base classes or provider/listener interfaces — which is a majority of .NET codebases using any inheritance-based framework (WinForms, WPF, WebForms, SignalR, Prism, MAUI, etc.).

Related

Environment

  • cdidx: v1.10.0 (/root/.local/bin/cdidx)
  • OS: Linux 4.4.0
  • Fixture inline in Repro section.

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