Skip to content

C# SymbolExtractor: class/struct/record signatures truncated to first physical line — multi-line base lists and where clauses are lost (downstream: ctor-chain base resolution, impact graph, analyze_symbol) #382

Description

@Widthdom

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

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