Skip to content

C# const fields with tuple types (public const (int, int) Pair = (1, 2);) emit phantom function const rows — const row :69 lacks tuple support, method regex :94 backtracks public into returnType and const into name #346

@Widthdom

Description

@Widthdom

Summary

A C# const field whose type is a tuple — public const (int, int) Pair = (1, 2);, public const (int a, int b) Named = ..., public const (int, int)? Maybe = null; — causes SymbolExtractor to (a) drop the field from the symbol index and (b) emit a phantom function const row pointing at the field line. Same backtracking mechanism as #336 (readonly-field-tuple → phantom function readonly), but on a different regex row and with a different visible phantom name (const instead of readonly), so searches for "phantom function const" don't surface #336.

#336 explicitly dismisses the const row in its root-cause section (":69 (const field) requires const. N/A."). Its proposed fix (negative lookahead on :94) would also eliminate this phantom, but the const row at :69 would still fail to capture the const-tuple field — the capture side needs a separate change.

Repro

CDIDX=/root/.local/bin/cdidx
mkdir -p /tmp/dogfood/cs-const-tuple
cat > /tmp/dogfood/cs-const-tuple/C.cs <<'EOF'
namespace ConstTuple;

public class Cfg
{
    // const tuple field — PHANTOM `function const`
    public const (int, int) Pair = (1, 2);

    // const named tuple — PHANTOM `function const`
    public const (int a, int b) NamedPair = (1, 2);

    // Control — captured correctly as `function Plain`
    public const int Plain = 42;

    // const nullable tuple — PHANTOM `function const`
    public const (int, int)? MaybePair = null;

    // Static readonly generic-over-tuple — DROPPED (same family, #336 scope)
    public static readonly System.Collections.Generic.List<(int, int)> Pairs = new();
}
EOF
"$CDIDX" index /tmp/dogfood/cs-const-tuple --rebuild
"$CDIDX" symbols --db /tmp/dogfood/cs-const-tuple/.cdidx/codeindex.db

Actual:

class      Cfg                                      C.cs:3-19
function   Plain                                    C.cs:12
namespace  ConstTuple                               C.cs:1
function   const                                    C.cs:6
function   const                                    C.cs:9
function   const                                    C.cs:15

Three phantom function const rows at L6, L9, L15. Pair, NamedPair, MaybePair, and Pairs are all missing from the symbol table. definition Pair --exact returns zero hits. definition const returns three rows of nonsense at field lines, none of which are actual callable methods.

Suspected root cause (from reading the source)

src/CodeIndex/Indexer/SymbolExtractor.cs:69 — const row:

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

The returnType char class [\w?.<>\[\],:]+ has no (, ), \s, and no \([^)]+\) tuple alternative. For public const (int, int) Pair = (1, 2);:

  1. Visibility matches public. Modifier slot (new|static) is empty (const itself is consumed next, literally).
  2. Literal const matches const.
  3. returnType char class tries to consume the next token — the next char is ( from (int, int). ( is not in the char class. returnType fails to consume anything meaningful (and the char class is + so it must match at least one char). Const row fails.

Then the method row at :94 runs. Same backtracking trace as #336, but the phantom comes out with a different name:

  1. With visibility=public, modifier={}: returnType tuple alt \([^)]+\) matches (int, int). \s+\w+ matches Pair. Then \s*(?:<[^>]+>\s*)?\( requires (, next is = (1, 2); — whitespace then =, not (. Fails.
  2. Visibility group backs off (empty). Modifier slot: const is not in the list (static|sealed|partial|readonly|unsafe|extern|virtual|override|abstract|async|new|file), so it stays empty.
  3. returnType char class matches public. \s+\w+ matches const. Then \s*(?:<[^>]+>\s*)?\( matches ( — the opening paren of the tuple type is consumed as the start of a parameter list.
  4. Match succeeds with returnType=public, name=const. Phantom emitted.

The nullable-tuple variant ((int, int)?) goes through the same path — the ? sits outside the \( the method regex consumes, so it never enters the match.

L18 (public static readonly List<(int, int)> Pairs) silently drops without a phantom because the char class in :71 can't cross the embedded (, and the method regex's own char class backtracking never reaches a valid \(. This drop is arguably a follow-up to #336's "defense in depth" note on :71, not a new bug.

Suggested direction

Two independent changes — either alone only addresses part of the symptom.

1. Extend the const row :69 to accept tuple / nullable-tuple / named-tuple return types.

Mirror the method row's returnType alternation:

(?<returnType>\([^)]+\)\??|(?:global::)?[\w?.<>\[\],:]+)

The \?? suffix handles nullable-tuple ((int, int)?). With this change, public const (int, int) Pair = ... is captured as a const with returnType (int, int) and name Pair.

2. Add the same visibility-keyword negative lookahead to the method row :94 proposed in #336.

(?<returnType>\([^)]+\)|(?:global::)?(?!(?:public|private|protected|internal)\b)[\w?.<>\[\],:]+)

After change (1), the method row at :94 no longer gets reached for const-tuple lines — but the #336 fix is still required to also eliminate the readonly-tuple phantom, tuple-return-delegate phantom (#340), tuple-return-operator phantom (#342), and this const-tuple phantom in older indexes or edge lines the extended const row still rejects.

Tests worth adding:

  • Every shape in the repro (plain const, const tuple, named const tuple, nullable const tuple, generic-over-tuple static readonly).
  • Assertion: no function const phantom ever appears.
  • Assertion: const-tuple fields are captured with returnType = (int, int) and name = Pair.
  • Regression: public const int X = 1; still captured as function X with returnType int.

Why it matters

  • public const (int, int) values are common in math / geometry / pinned-point code (unit vectors, corner indices, board dimensions). Each one pollutes the index with a phantom function const row and hides the real Pair / NamedPair name.
  • cdidx definition 'Pair' misses the const field; an AI agent asked "what's the value of Pair?" sees nothing, while cdidx definition 'const' returns meaningless phantoms.
  • hotspots --kind function over-counts phantom const rows. unused --kind function lists them as unused functions.

Cross-language note

C#-specific. The const-with-tuple shape is unique to C# syntax. The fix is C#-scoped to rows :69 and :94.

Scope

Related

Environment

  • cdidx: v1.10.0 (/root/.local/bin/cdidx).
  • Platform: linux-x64.
  • 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