Summary
src/CodeIndex/Indexer/SymbolExtractor.cs stores SymbolRecord.Signature using only the single physical line where the declaration regex matched (signature = line[absoluteStartColumn..].Trim(); at SymbolExtractor.cs:849). For C# class / struct / record / interface declarations the base list and generic constraints are routinely wrapped to following lines, especially in larger libraries:
public sealed class Foo<T>
: BaseFoo<T>, IBar, IBaz
where T : class, new()
{
public Foo(int x) : base(x) { }
}
After indexing, symbols.signature for Foo is just public sealed class Foo<T> — the : and the base list never enter the signature column. Anything that reads SymbolRecord.Signature to recover the base type silently degrades to "no base type."
Observed impact
Minimal reproduction
Index any project containing a class whose header is wrapped onto two or more lines (very common in ASP.NET Core / Roslyn / EF Core / xUnit / Microsoft.Extensions.*) and inspect:
dotnet ./src/CodeIndex/bin/Debug/net8.0/cdidx.dll inspect Foo --body --json
The returned definition.signature ends at class Foo<T>; analyze_symbol results for constructors in that class show a missing base(...) caller edge.
Root cause
SymbolExtractor.cs:849 — single-line slice for the non-property branch:
var signature = sameLineEndColumn >= absoluteStartColumn
? line[absoluteStartColumn..(sameLineEndColumn + 1)].Trim()
: lang == \"csharp\" && pattern.Kind == \"property\" && csharpPropertyCandidate.LastConsumedLineIndex > i
? BuildCSharpMultilineSignature(...)
: line[absoluteStartColumn..].Trim(); // <-- truncates at newline
Properties already have a BuildCSharpMultilineSignature path. Classes/structs/records/interfaces do not — they always fall to line[absoluteStartColumn..].Trim().
Suggested fix direction
Extend the existing multi-line-signature path to C# type declarations, consuming lines until the opening { or ; (primary-ctor records) is reached, while respecting:
- parenthesis depth (primary ctor parameter lists),
- angle-bracket depth (generic parameters / constraints),
where continuation clauses,
- the same line-limit cap used by
HeaderScan so we don't run away on unterminated input.
Storing the full header unlocks:
Acceptance criteria
symbols.signature for a wrapped C# class/struct/record/interface contains the base list and any where clauses up to (but not including) the body opener.
ReferenceExtractor.ParseCSharpBaseType resolves the correct A<T> for the wrapped example above, producing a : base(...) call reference whose target is A.
- New
SymbolExtractorTests case for a wrapped class header; new ReferenceExtractorTests case exercising base(...) inside a wrapped-header class.
- No regression on same-line headers or the existing multi-line property path (
BuildCSharpMultilineSignature).
Related
Summary
src/CodeIndex/Indexer/SymbolExtractor.csstoresSymbolRecord.Signatureusing only the single physical line where the declaration regex matched (signature = line[absoluteStartColumn..].Trim();atSymbolExtractor.cs:849). For C# class / struct / record / interface declarations the base list and generic constraints are routinely wrapped to following lines, especially in larger libraries:After indexing,
symbols.signatureforFoois justpublic sealed class Foo<T>— the:and the base list never enter the signature column. Anything that readsSymbolRecord.Signatureto recover the base type silently degrades to "no base type."Observed impact
ReferenceExtractor.ParseCSharpBaseType(enclosingType.Signature)(added in the C#: constructor chain calls: this(...)and: base(...)are never recorded as references —callerson a constructor misses every subclass and every overload #257 constructor-chain fix atReferenceExtractor.cs:461) returnsnullfor every wrapped-header C# class.base(...)initializer calls inside that class then skip the reference row, re-introducing the exact gap that C#: constructor chain calls: this(...)and: base(...)are never recorded as references —callerson a constructor misses every subclass and every overload #257 was supposed to close whenever the class header is wrapped.is/aspatterns, attributes) never registered as references #256),impact,analyze_symbol, or any lint/inspection that wants to answer "what does this class extend?" from the index alone.Minimal reproduction
Index any project containing a class whose header is wrapped onto two or more lines (very common in ASP.NET Core / Roslyn / EF Core / xUnit / Microsoft.Extensions.*) and inspect:
The returned
definition.signatureends atclass Foo<T>;analyze_symbolresults for constructors in that class show a missingbase(...)caller edge.Root cause
SymbolExtractor.cs:849— single-line slice for the non-property branch:Properties already have a
BuildCSharpMultilineSignaturepath. Classes/structs/records/interfaces do not — they always fall toline[absoluteStartColumn..].Trim().Suggested fix direction
Extend the existing multi-line-signature path to C# type declarations, consuming lines until the opening
{or;(primary-ctor records) is reached, while respecting:wherecontinuation clauses,HeaderScanso we don't run away on unterminated input.Storing the full header unlocks:
: this(...)and: base(...)are never recorded as references —callerson a constructor misses every subclass and every overload #257 base-call resolution on wrapped headers,is/aspatterns, attributes) never registered as references #256) with accurate base/interface targets,Acceptance criteria
symbols.signaturefor a wrapped C# class/struct/record/interface contains the base list and anywhereclauses up to (but not including) the body opener.ReferenceExtractor.ParseCSharpBaseTyperesolves the correctA<T>for the wrapped example above, producing a: base(...)call reference whose target isA.SymbolExtractorTestscase for a wrapped class header; newReferenceExtractorTestscase exercisingbase(...)inside a wrapped-header class.BuildCSharpMultilineSignature).Related
: this(...)and: base(...)are never recorded as references —callerson a constructor misses every subclass and every overload #257 (constructor chain references — base-type resolution depends on this)is/aspatterns, attributes) never registered as references #256 (type-position identifiers as references)