Skip to content

C#/Java: nameof(X.Y) / typeof(T) / default(T) arguments are silently dropped from the reference index #253

@Widthdom

Description

@Widthdom

Summary

In C# (and analogously Java's T.class / Class.forName("T")), the keywords nameof, typeof, sizeof, and default accept a type or member name as their argument. These arguments are first-class compile-time references: nameof(Target.Alpha) breaks at compile time if Target.Alpha is renamed, and refactoring tools treat the argument as a rename target. Yet cdidx's reference extractor drops them entirely because:

  1. nameof, typeof, sizeof are in IgnoredCallNames (ReferenceExtractor.cs:27), so the name( call match is correctly suppressed — but that leaves the inner identifier(s) to be matched only via CallRegex, which requires a trailing (. Target.Alpha) has no ( after Alpha, so it never matches.
  2. There is no special-case handling for these keyword-like operators to peek at their argument list.

Net effect: references Target, references Alpha, references Beta on code that references those members via nameof/typeof/default return zero hits.

Repro

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

public class Target
{
    public int Alpha() => 1;
    public static int Beta() => 2;
}

public class Caller
{
    public void Work()
    {
        var n1 = nameof(Target.Alpha);   // → should be a reference to Target and Alpha
        var n2 = nameof(Target);         // → should be a reference to Target
        var n3 = nameof(Beta);           // → should be a reference to Beta

        var tp = typeof(Target);         // → should be a reference to Target
        Target? def = default(Target);   // → should be a reference to Target
    }
}
EOF
"$CDIDX" index /tmp/dogfood/cs-nameof --rebuild
"$CDIDX" references Target --db /tmp/dogfood/cs-nameof/.cdidx/codeindex.db --exact
"$CDIDX" references Alpha  --db /tmp/dogfood/cs-nameof/.cdidx/codeindex.db --exact
"$CDIDX" references Beta   --db /tmp/dogfood/cs-nameof/.cdidx/codeindex.db --exact

Observed:

No references found.        ← Target (5 occurrences)
No references found.        ← Alpha (1 occurrence)
No references found.        ← Beta  (1 occurrence)

Expected: at minimum one reference row per argument of nameof/typeof/default.

Suspected root cause (from reading the source)

src/CodeIndex/Indexer/ReferenceExtractor.cs:

  • Line 27: IgnoredCallNames includes "sizeof", "typeof", "return", "throw", "nameof", "await", "using", "new".
  • Line 76: CallRegex = (?<![\w$])(?<name>[A-Za-z_]\w*)(?:<[^>\n]+>)?\s*\( — requires trailing (.
  • Line 155-164: only CallRegex.Matches(preparedLine) is consulted for regular references.

So the scanner walks each call-like token. For nameof(Target.Alpha) it sees:

  • nameof( — matches CallRegex, but nameof is in IgnoredCallNames → suppressed. ✓ correct for the keyword itself.
  • Target.Alpha) — no trailing ( anywhere, so CallRegex finds nothing. The argument is never scanned for identifiers.

Analogously for typeof(Target) and default(Target) (default is in the set at line 34).

Suggested fix

Add a targeted argument-capture pass that, for each (nameof|typeof|sizeof|default)\s*\(\s*([\w.]+)\s*\) match on the prepared line, emits one reference row per dotted segment:

private static readonly Regex NameofTypeofArgRegex = new(
    @"\b(?:nameof|typeof|sizeof|default)\s*\(\s*(?<arg>[\w.]+)(?:\[\])?\s*\)",
    RegexOptions.Compiled);

// Inside the csharp branch of the scan:
if (language is "csharp")
{
    foreach (Match arg in NameofTypeofArgRegex.Matches(preparedLine))
    {
        var raw = arg.Groups["arg"].Value;
        // Emit a reference for each dot-segment: `Target.Alpha` → Target + Alpha.
        foreach (var segment in raw.Split('.'))
        {
            if (IgnoredCallNames.Contains(segment)) continue;
            AddReference(references, seen, fileId, /* name: */ segment,
                         /* column: */ arg.Index + raw.IndexOf(segment),
                         "type_reference", context, lineNumber, container);
        }
    }
}

Use a dedicated reference_kind (e.g. "type_reference" or "nameof_arg") so downstream tools can distinguish these from ordinary calls. callers/callees should continue to ignore them; references and impact (impact already reads reference_kind IN (...) filtered) should include them.

Java has a parallel case with SomeType.class, MyType[].class, and Class.forName("Foo") — a similar regex (\b(?<arg>[\w.]+)\s*\.class\b) would cover the common compile-time-reference style.

Why it matters

  • Refactoring safety. The whole point of nameof(Foo) over "Foo" is that renaming Foo breaks at compile time. Users who search references Foo to see "what will this rename break?" miss every nameof caller today.
  • unused false positives. A member only referenced via nameof(MyMember) (e.g. for property-change notifications, serialization attribute keys, logging) looks unused.
  • impact chains truncate early. If a public API is only reached through nameof for attribute-based wiring (e.g. [BindProperty(Name = nameof(Foo))]), the impact analysis drops the edge.

Related

Scope

  • src/CodeIndex/Indexer/ReferenceExtractor.cs — add argument-capture pass for C# (and parallel Java .class).
  • tests/CodeIndex.Tests/ReferenceExtractorTests.cs — fixtures above.
  • Consider a dedicated reference_kind label so existing callers/callees semantics are preserved.

Environment

  • cdidx v1.10.0 (installed via install.sh to /root/.local/bin/cdidx).
  • 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