Skip to content

C#: static abstract / abstract static interface operators (C# 11 generic math) are dropped from the symbol index #244

@Widthdom

Description

@Widthdom

Summary

C# 11 allows interfaces to declare static abstract (or abstract static) operator members — the foundation of System.Numerics.INumber<TSelf> generic math:

public interface IMath<T> where T : IMath<T>
{
    static abstract T operator +(T a, T b);        // static-first
    abstract static T operator -(T a, T b);        // abstract-first (also valid)
    static abstract implicit operator T(int x);    // conversion operator
    abstract static T Zero { get; }                // captured correctly — baseline
    abstract static int Compare(T a, T b);         // captured correctly — baseline
}

The C# symbol extractor silently drops every operator line on the interface (both modifier orders, both regular operators and implicit/explicit conversion operators), while the same operators declared on a class or struct are captured. Properties and methods on the same interface are captured correctly — it is specifically the operator regexes that are broken.

Repro

CDIDX=/root/.local/bin/cdidx
mkdir -p /tmp/dogfood/cs-iop
cat > /tmp/dogfood/cs-iop/I.cs <<'EOF'
namespace Demo;

public interface IMath<T> where T : IMath<T>
{
    static abstract T operator +(T a, T b);        // line 5   static-first
    abstract static T operator -(T a, T b);        // line 6   abstract-first
    static abstract T operator *(T a, T b);        // line 7   static-first
    abstract static T Zero { get; }                // line 8   property (captured)
    static abstract implicit operator T(int x);    // line 9   conversion operator
    abstract static int Compare(T a, T b);         // line 10  method (captured)
}

public struct N
{
    public static N operator +(N a, N b) => a;     // line 15  captured
    public static N operator -(N a, N b) => a;     // line 16  captured
}
EOF
"$CDIDX" /tmp/dogfood/cs-iop
"$CDIDX" symbols --db /tmp/dogfood/cs-iop/.cdidx/codeindex.db --lang csharp

Actual

function   +          I.cs:15       # struct N
function   -          I.cs:16       # struct N
interface  IMath      I.cs:3-11
struct     N          I.cs:13-17
function   Compare    I.cs:10       # baseline method — captured
property   Zero       I.cs:8        # baseline property — captured
namespace  Demo       I.cs:1

Missing: lines 5, 6, 7, 9 — all four interface operator declarations. Expected: every operator line to be captured the way line 15/16 are on the struct.

Downstream effects:

  • cdidx definition "+" --db ... on IMath<T> returns no hits even though the interface declares operator + on line 5.
  • cdidx callers / cdidx references cannot navigate through the interface's operator surface at all.
  • cdidx outline I.cs hides half of IMath<T>'s members — AI clients browsing a generic-math API see Zero and Compare but no arithmetic.

Suspected root cause (from reading the source)

Two regexes in src/CodeIndex/Indexer/SymbolExtractor.cs assume static is the first (and only) pre-return-type modifier, and allow exactly one return-type token between static and operator:

SymbolExtractor.cs:84 — implicit/explicit conversion operator:

@"^\s*(?:(?<visibility>public|...|internal)\s+)?static\s+(?<name>implicit|explicit)\s+operator\b"

Requires static <name> operator. On an interface the line is static abstract implicit operator T(int x); — extra abstract between static and implicit breaks the match. Also, abstract static implicit operator T(...) (abstract-first) starts with abstract, which the pattern cannot handle at all.

SymbolExtractor.cs:87 — binary/unary operator:

@"^\s*(?:(?<visibility>public|...|internal)\s+)?static\s+\S+\s+operator\s+(?<name>\S+)\s*\("

Requires static <one-token> operator. Fails two ways on interfaces:

  • static abstract T operator +(T a, T b); — between static and operator there are two tokens (abstract and T), but \S+\s+operator only allows one.
  • abstract static T operator +(T a, T b); — line starts with abstract, not static, so the required static literal at the front fails.

(abstract static int Compare(T a, T b) on line 10 of the repro is captured because the method regex on SymbolExtractor.cs:94 has a general modifier slot (?:(?:static|sealed|partial|readonly|unsafe|extern|virtual|override|abstract|async|new|file)\s+)* — that same slot is absent from both operator regexes.)

Suggested fix

Give both operator regexes the same permissive modifier slot the method regex already has, and allow the modifiers to appear in any order.

Conversion operator (SymbolExtractor.cs:84):

@"^\s*(?:(?<visibility>public|private|protected\s+internal|private\s+protected|protected|internal)\s+)?(?:(?:static|abstract|virtual|sealed|override|extern|unsafe|new)\s+)*(?<name>implicit|explicit)\s+operator\b(?:\s+checked)?"

Binary/unary operator (SymbolExtractor.cs:87):

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

The (?:checked\s+)? slot is the same addition called out by #238, so fixing both at once is natural. A static keyword is still required on a legal operator declaration at the syntactic level, but in practice the modifier slot already requires at least one modifier via the non-empty name/body that follows; requiring the explicit static literal inside the modifier slot is the cleanest way to keep call-site lines out (x operator +(y) is not a C# statement). The existing static\s+ anchor can simply be dropped in favor of letting the modifier slot consume it — the operator literal plus ( at the end is what actually anchors this regex to a real declaration.

Optionally stash the modifiers (abstract, static, checked) in a signature/metadata field so definition + can disambiguate same-named operator rows in output. Not required for capture correctness.

Why it matters

  • System.Numerics.INumber<TSelf>, IBinaryInteger, IComparisonOperators, IAdditionOperators, ISubtractionOperators, IMultiplyOperators, IDivisionOperators, IModulusOperators, IUnaryNegationOperators, IEqualityOperators, IIncrementOperators, IBitwiseOperators — every generic-math interface in System.Numerics relies on static abstract T operator ?(...). Today cdidx sees only the Parse / TryParse / Compare helpers on those interfaces, not the operators.
  • Custom numeric types (money, units of measure, saturating integers) declared against INumber<TSelf> expose their entire arithmetic contract on an interface — it is invisible to cdidx.
  • The bug is silent. No warning, no fallback, no lower-confidence capture — just missing rows.

Scope

  • src/CodeIndex/Indexer/SymbolExtractor.cs — loosen the two operator regexes (lines 84 and 87) to accept a modifier slot in any order and the C# 11 checked keyword.
  • tests/CodeIndex.Tests/SymbolExtractorTests.cs — fixtures for static abstract T operator +, abstract static T operator -, static abstract implicit operator T(int), on both interface and class/struct.
  • DEVELOPER_GUIDE.md language-pattern reference table — add C# 11 static abstract interface members.

Related

Environment

  • cdidx: v1.10.0 (tarball from GitHub releases).
  • 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