Skip to content

C# methods with a generic attribute applied to a type parameter (void M<[GenAttr<int>] T>(T t)) are silently dropped — the generic-param group <[^>]+> cannot cross the nested > inside the attribute #347

@Widthdom

Description

@Widthdom

Summary

A C# method whose generic type-parameter list contains a generic attribute[GenAttr<int>], [GenAttr<(int, int)>], [MyAttr<T>, OtherAttr<U>] — is silently dropped from the symbol index. Example:

public void M<[GenAttr<int>] U>(U u) { }
public void N<[GenAttr<int>, GenAttr<string>] U>(U u) { }

Plain (non-generic) attributes on the same type parameter continue to work:

public void Ok1<[DynamicallyAccessedMembers(...)] T>(T t) { }      // captured ✓
public void Ok2<[NotNull] T>(T t) { }                              // captured ✓
public void Ok3<[System.Obsolete, NotNull] T>(T t) { }             // captured ✓

Generic attributes shipped in C# 11 (.NET 7+) and are increasingly idiomatic for trim-safe / AOT-safe APIs. They are supported on the method itself (attribute above the method — stripped by StripLeadingCSharpAttributeLists), but not inside the method's own generic type-parameter list.

Repro

CDIDX=/root/.local/bin/cdidx
mkdir -p /tmp/dogfood/cs-generic-attr
cat > /tmp/dogfood/cs-generic-attr/G.cs <<'EOF'
namespace GenericAttr;

public class GenAttr<T> : System.Attribute { }

[GenAttr<int>]
public class Tagged
{
    [GenAttr<string>]
    public int A() => 0;            // captured ✓ (leading generic attr is stripped)

    [GenAttr<(int, int)>]
    public int B() => 0;            // captured ✓

    // Attribute on type parameter — DROPPED
    public void M<[GenAttr<int>] U>(U u) { }

    // Multiple generic attributes on type parameter — DROPPED
    public void N<[GenAttr<int>, GenAttr<string>] U>(U u) { }
}
EOF
"$CDIDX" index /tmp/dogfood/cs-generic-attr --rebuild
"$CDIDX" symbols --db /tmp/dogfood/cs-generic-attr/.cdidx/codeindex.db

Actual:

function   A                                        G.cs:10
function   B                                        G.cs:13
class      GenAttr                                  G.cs:3
class      Tagged                                   G.cs:7-20
namespace  GenericAttr                              G.cs:1

M and N are missing. definition M --exact / definition N --exact return zero hits.

Suspected root cause (from reading the source)

src/CodeIndex/Indexer/SymbolExtractor.cs:94 (method regex):

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

The optional generic-parameter group is <[^>]+>. [^>]+ is greedy but cannot cross a >. Trace for public void M<[GenAttr<int>] U>(U u) { }:

  1. Visibility public, modifiers empty, returnType void, name M.
  2. Generic group <[^>]+>:
    • < matches <.
    • [^>]+ matches [GenAttr<int.
    • > matches the > that closes GenAttr<int>.
  3. \s* matches 0 chars. \( required; cursor is at ] U>(U u) { }. Fails.
  4. Regex backtracks. Generic group is optional, tries empty — then \s*\( after name M needs (; cursor is at <.... Fails.
  5. No other C# row matches this shape (no this[, no . before name, etc.). Silent drop.

The non-generic attribute cases work because the attribute body contains no > to prematurely terminate the [^>]+ match — e.g. [NotNull] U> keeps [^>]+ chugging through [NotNull] U until the real closing > of the type-parameter list.

Suggested direction

Replace the shallow generic-param matcher with a balanced-bracket capture. Two options:

(A) Allow one level of nested <...> inside the generic group — the smallest regex change that handles single-level generic attributes (the dominant real-world case):

(?:<(?:[^<>]|<[^<>]*>)*>\s*)?

This matches the outer <...> where the body can contain arbitrary non-angle characters or a single balanced <...> pair (e.g. GenAttr<int>). It will still fail on doubly-nested generic attributes ([GenAttr<Pair<int, int>>]), but those are vanishingly rare.

(B) Use a full balanced-bracket helper via a small counter-aware pre-pass that finds the end of <...> with proper nesting, then feed the remainder to the rest of the regex. Larger change but correct for all nesting depths; shares code with the returnType tokenizer family (#222 / #241 / #263).

Tests worth including:

  • M<[GenAttr<int>] U>(U u) — single generic attribute.
  • N<[GenAttr<int>, GenAttr<string>] U>(U u) — multiple generic attributes.
  • P<[GenAttr<(int, int)>] U>(U u) — generic attribute with tuple argument.
  • Regression: M<T>(T t) / M<[NotNull] T>(T t) / M<[Obsolete, NotNull] T>(T t) still capture.

Why it matters

  • .NET 7+ trim/AOT-safe APIs use [DynamicallyAccessedMembers<T>]-style generic attributes on type parameters for reflection-safe generic helpers. Each such method vanishes from the symbol index today.
  • hotspots --kind function and unused --kind function under-report in trim-safe libraries; definition misses by exact name.
  • Silent — no warning; the developer only notices when asking for a specific method by name.

Cross-language note

  • Java's parallel feature (type-use annotations on type parameters: <@NonNull T>) goes through the regular method row. The Java method regex's generic-param handling is \w+(?:<[^>]+>)?(?:\[\])? on the returnType only; type-parameter lists aren't matched by it at all, so Java isn't affected in the same way.
  • Kotlin / Swift / Rust use different generic-parameter attribute syntax and are unaffected.

Scope

  • src/CodeIndex/Indexer/SymbolExtractor.cs:94 — widen the generic-parameter matcher to accept one level of nested <...>, or switch to a balanced-bracket helper.
  • tests/CodeIndex.Tests/SymbolExtractorTests.cs — fixtures for each shape above.

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