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.
Summary
The C# event regex only accepts
visibility?andstatic?as modifiers before theeventkeyword. In real C# codebases, events very commonly carry other modifiers —abstractin abstract-class contracts,virtual/override/sealedin inheritance chains,newfor hiding, andstatic abstract/static virtualin C# 11 interface contracts. All of these silently drop the event from the symbol index. Only plainpublic event …andprotected event …(visibility-only) andstatic event …survive. This is a very common real-world pattern — any abstract base class or interface event contract is invisible tosymbols,definition,outline,references, etc.Repro
Expected: 9 event rows. Observed: 3 (
Peek,Plain,Regular). The six events withabstract,virtual,override,sealed override,new,static abstract,static virtualmodifiers are silently dropped.Suspected root cause
src/CodeIndex/Indexer/SymbolExtractor.cs:107— the C# event regex:The pre-
eventmodifier slot consumes only an optional visibility and an optionalstatic. Anything else —abstract,virtual,override,sealed,new,extern,unsafe,file— blocks the match because the regex requires theeventkeyword immediately afterstatic?. Compare with the method regex at:94whose modifier slot includesvirtual|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 likesealed overrideandstatic abstractare both accepted.Consider whether
requiredshould be in the list too —requiredis not valid on an event per the C# spec (only on fields/properties), so it can be omitted. Likewisereadonlyis 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
src/CodeIndex/Indexer/SymbolExtractor.cs:107. Downstream:symbols,definition,outline,references,callers,inspect,map.Related
static abstract/abstract staticinterface operators (C# 11 generic math) are dropped from the symbol index #244 —static abstract/abstract staticinterface operators dropped. Same modifier-slot family, different row (method row). This issue is the event-row analogue.public partial string Name { get; set; }) silently dropped —partialnot in property-regex modifier list #228 —partialproperty modifier missing. Same pattern: modifier slot narrower than real-world grammar.ref/ref readonlyreturn types on methods and properties are silently dropped —public ref T Find(...)produces zero symbols #224 —ref/ref readonlyreturn types dropped on methods and properties. Related returnType char-class family.{ internal get; set; }) is silently dropped #332 — property accessor visibility. Adjacent but different regex row.Environment
/root/.local/bin/cdidx)