Skip to content

C# 13 partial indexers (public partial int this[int i] { get; set; }) are silently dropped — partial not in indexer-regex modifier list #350

@Widthdom

Description

@Widthdom

Summary

C# 13 extended the partial modifier beyond methods to properties and indexers. SymbolExtractor added partial to the class / struct / interface / method regex modifier lists, but not to the property regexes (tracked separately in #228) and not to the indexer regex at :118. As a result, every partial indexer declaration and every matching implementation is silently dropped from the symbol index, even though partial indexers are a real, shipped C# 13 feature used alongside partial properties by source generators.

public partial class Svc
{
    // Declaring part — DROPPED
    public partial int this[int i] { get; set; }
    public partial string this[string key] { get; }
}

public partial class Svc
{
    // Implementing part — DROPPED
    public partial int this[int i]
    {
        get => 0;
        set { }
    }

    // Expression-bodied implementation — DROPPED
    public partial string this[string key] => "x";
}

definition this --exact, symbols --kind function filtered to the class, and inspect on the enclosing class all miss every partial indexer row. The non-partial baseline indexer (public int this[long key] { get => 0; set { } }) is captured, so the gap is specific to the partial modifier on the indexer grammar — not to the indexer regex overall.

This is distinct from #228 (partial properties). #228's scope is explicitly SymbolExtractor.cs:100 and :103 — the two property regexes. The indexer lives in its own regex at :118 with its own modifier list, so #228's fix would not cover the indexer row. Filing separately so the fix lands in the right regex.

Repro

CDIDX=/root/.local/bin/cdidx
mkdir -p /tmp/dogfood/cs-13-partial-indexer
cat > /tmp/dogfood/cs-13-partial-indexer/M.cs <<'EOF'
namespace Cs13PartialIndexer;

public partial class Svc
{
    // Declarations
    public partial int this[int i] { get; set; }
    public partial string this[string key] { get; }

    // Baseline: non-partial indexer (CAPTURED)
    public int this[long key] { get => 0; set { } }

    // Baseline: partial method (CAPTURED)
    public partial int M();
}

public partial class Svc
{
    // Partial indexer impl with block accessors
    public partial int this[int i]
    {
        get => 0;
        set { }
    }

    // Partial indexer impl with expression body
    public partial string this[string key] => "x";

    // Baseline: partial method impl (CAPTURED)
    public partial int M() => 42;
}
EOF
"$CDIDX" index /tmp/dogfood/cs-13-partial-indexer --rebuild
"$CDIDX" symbols --db /tmp/dogfood/cs-13-partial-indexer/.cdidx/codeindex.db

Observed:

function   M                                        M.cs:13       ← partial method decl, CAPTURED
function   M                                        M.cs:31       ← partial method impl, CAPTURED
class      Svc                                      M.cs:3-14
class      Svc                                      M.cs:16-32
function   this                                     M.cs:10       ← non-partial indexer, CAPTURED
namespace  Cs13PartialIndexer                       M.cs:1
(6 symbols in 1 files)

Missing: the partial indexer declarations at M.cs:6, M.cs:7, and their implementations at M.cs:19-23, M.cs:26 — 4 out of 5 indexer rows silently lost. definition this --exact returns only the non-partial baseline, making symbols --kind function, callers, and inspect materially wrong on any source-generator-heavy C# 13 codebase that uses partial indexers.

Suspected root cause

src/CodeIndex/Indexer/SymbolExtractor.cs:118 — the C# indexer row:

new("function",  new Regex(
    @"^\s*(?:(?<visibility>public|private|protected\s+internal|private\s+protected|protected|internal)\s+)?"
  + @"(?:(?:static|virtual|override|abstract|sealed|new)\s+)*"
  + @"(?<returnType>(?:global::)?[\w?.<>\[\],:]+)\s+(?<name>this)\s*\[",
    RegexOptions.Compiled),
    BodyStyle.Brace, "visibility", "returnType"),

The modifier alternation is static|virtual|override|abstract|sealed|new. Missing: partial (also arguably missing required and extern, but partial is the C# 13 canonical case).

Walkthrough for public partial int this[int i] { get; set; }:

  1. visibility matches public.
  2. (?:(?:static|virtual|override|abstract|sealed|new)\s+)* tries to match partial. partial is not in the list → zero iterations.
  3. Cursor at partial int this[int i] { get; set; }.
  4. returnType (?:global::)?[\w?.<>\[\],:]+ greedy matches partial.
  5. \s+ matches space.
  6. (?<name>this) expects the literal string this — next is intfails.
  7. Regex backtracks: shorter returnType (partia, parti, …) all followed by non-whitespace chars → none lead to \s+this.
  8. Whole regex fails.

No other C# pattern in the cache matches the indexer shape:

  • Method regex at :94 requires \s*\( after the name — but here we have \s*\[.
  • Explicit-interface regex at :116 requires a dot between interface type and name — this has no dot prefix.
  • Property regexes :100 / :103 require \w+ after return type, but this is matched literally only by the indexer regex.

Net: silent drop. No warning, no phantom.

Methods with partial are fine because the method regex at :94 already includes partial in its modifier list. Properties with partial are covered by #228's scope, which proposes adding partial to :100 / :103. Indexers fall between the two and need the same one-word addition in :118.

Suggested direction

(A) Add partial to the indexer regex modifier alternation at :118:

// :118 — change
(?:(?:static|virtual|override|abstract|sealed|new)\s+)*
// to
(?:(?:static|virtual|override|abstract|sealed|new|partial|required|extern)\s+)*

partial is the primary fix (C# 13). required is not legal on indexers per the spec, but matches the property row's modifier set and is harmless. extern covers the interop case.

(B) Same expansion to the event regex at :107 for consistency:

// :107 — change
(?:(?:static)\s+)?event\s+\S+\s+(?<name>\w+)
// to
(?:(?:static|partial|virtual|override|abstract|sealed|new|extern|unsafe)\s+)*event\s+\S+\s+(?<name>\w+)

This adds partial support for partial events (adjacent C# feature, not strictly C# 13 but same grammar shape). #334 tracks the abstract / virtual / override / sealed / new slice of this expansion — partial would be an additional entry in the same list change.

(C) Fold this into #228's approach: if #228's fix lands first, reviewing the entire C# row block and sweeping in partial across :100, :103, :107, :118 in one commit avoids relanding the same file several times.

Preferred: (A) as the focused, narrow fix for this ticket, and (B) as the adjacent hardening on the event row. Either can land independently.

Regression fixtures:

  • public partial int this[int i] { get; set; } → captured as indexer row.
  • public partial int this[int i] { get => 0; set { } } → captured.
  • public partial string this[string key] => "x"; → captured.
  • public int this[long key] { get => 0; set { } } → still captured (baseline).
  • public static int this[int i] { get; } → still captured (if/where legal; Roslyn allows compile errors, but the regex already handles static).

Why it matters

  • Partial indexers are a shipped C# 13 feature, used together with partial properties by source generators (CommunityToolkit.Mvvm, Roslyn analyzers, internal framework code). A file that exercises partial indexers drops every such declaration silently.
  • Silent drop. definition this, outline, inspect, callers, hotspots --kind function all under-report on partial-indexer files. No warning surfaces to the user.
  • Graph completeness. Impact analysis and caller/callee traversal miss every indexer access site that targets a partial indexer, because the symbol-side definition is absent.
  • Source-generator ViewModels. MVVM codebases with [ObservableProperty]-style generators that include indexer overloads lose their indexer members entirely.
  • Single-word regex fix. Low-risk, matches the precedent established by C# 13 partial properties (public partial string Name { get; set; }) silently dropped — partial not in property-regex modifier list #228 for properties.

Cross-language note

Partial indexers are C#-specific grammar. Not applicable to other languages.

Adjacent: partial events (covered by expansion (B) above) — legality varies by C# version and is less clean-cut than partial indexers, but the regex gap is the same shape, so (B) is worth folding in alongside (A). #334 is the existing pivot for expanding the event regex modifier list; this issue nudges that expansion to also cover partial.

Scope

  • src/CodeIndex/Indexer/SymbolExtractor.cs:118 — indexer regex modifier list — add partial (and optionally required / extern).
  • src/CodeIndex/Indexer/SymbolExtractor.cs:107 — event regex modifier list — expand to include partial (overlaps with C# events with abstract / virtual / override / sealed / new modifiers are silently dropped from the symbol index #334's scope).
  • tests/CodeIndex.Tests/SymbolExtractorTests.cs — fixtures as listed in the Suggested direction section, plus regression for the existing non-partial indexer.
  • DEVELOPER_GUIDE.md language-pattern reference table — C# row's indexer entry should list partial as a supported modifier.

Related

Environment

  • cdidx: v1.10.0 (/root/.local/bin/cdidx).
  • Platform: linux-x64.
  • Fixture: /tmp/dogfood/cs-13-partial-indexer/M.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