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) { }:
- Visibility
public, modifiers empty, returnType void, name M.
- Generic group
<[^>]+>:
< matches <.
[^>]+ matches [GenAttr<int.
> matches the > that closes GenAttr<int>.
\s* matches 0 chars. \( required; cursor is at ] U>(U u) { }. Fails.
- Regex backtracks. Generic group is optional, tries empty — then
\s*\( after name M needs (; cursor is at <.... Fails.
- 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.
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:Plain (non-generic) attributes on the same type parameter continue to work:
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
Actual:
MandNare missing.definition M --exact/definition N --exactreturn zero hits.Suspected root cause (from reading the source)
src/CodeIndex/Indexer/SymbolExtractor.cs:94(method regex):The optional generic-parameter group is
<[^>]+>.[^>]+is greedy but cannot cross a>. Trace forpublic void M<[GenAttr<int>] U>(U u) { }:public, modifiers empty, returnTypevoid, nameM.<[^>]+>:<matches<.[^>]+matches[GenAttr<int.>matches the>that closesGenAttr<int>.\s*matches 0 chars.\(required; cursor is at] U>(U u) { }. Fails.\s*\(after nameMneeds(; cursor is at<.... Fails.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] Uuntil 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):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.M<T>(T t)/M<[NotNull] T>(T t)/M<[Obsolete, NotNull] T>(T t)still capture.Why it matters
[DynamicallyAccessedMembers<T>]-style generic attributes on type parameters for reflection-safe generic helpers. Each such method vanishes from the symbol index today.hotspots --kind functionandunused --kind functionunder-report in trim-safe libraries;definitionmisses by exact name.Cross-language note
<@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.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
new Dict<K, List<V>>()/Foo<Bar<int>>()are silently dropped from the reference index — the>>tail breaks the generic-arg regex #263 — nested generics in call-site references (Foo<Bar<int>>()) drop; same>>tail breaks a different regex (CallRegex). Shared root: shallow angle-bracket matchers.Task<Result<A, B>>,Dictionary<K, V>) are silently dropped — idiomatic .NET formatting is effectively unindexed #222 — method return-type generic-argument-space handling (same returnType tokenizer family).Task<(int, string)>,Dictionary<string, (int x, int y)>) are dropped from the symbol index #241 — generic-over-tuple return types.Environment
/root/.local/bin/cdidx).CLOUD_BOOTSTRAP_PROMPT.md.