From c65de070db53280726f0b81387eec54f71157259 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sun, 24 May 2026 18:37:51 +0900 Subject: [PATCH 1/2] Fix C# generic type argument graph edges (#2062) --- DEVELOPER_GUIDE.md | 16 ++--- changelog.d/unreleased/2062.fixed.md | 20 ++++++ .../Database/DbReader.GraphQueries.cs | 3 +- src/CodeIndex/Database/DbReader.cs | 6 +- .../ReferenceExtractor.TypeReferences.cs | 70 ++++++++++++++++++- tests/CodeIndex.Tests/DbReaderTests.cs | 26 +++++++ .../ReferenceExtractorTests.cs | 25 +++++++ 7 files changed, 152 insertions(+), 14 deletions(-) create mode 100644 changelog.d/unreleased/2062.fixed.md diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index 22765eb02b..b7210a53ed 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -228,7 +228,7 @@ symbol_references ( id INTEGER PRIMARY KEY AUTOINCREMENT, file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, symbol_name TEXT, -- referenced symbol name - reference_kind TEXT, -- "call", "instantiate", "subscribe", "razor_event_binding", "friend", "attribute", "annotation", "decorator", "type_reference", "implicit_implementation" + reference_kind TEXT, -- "call", "instantiate", "generic_type_argument", "subscribe", "razor_event_binding", "friend", "attribute", "annotation", "decorator", "type_reference", "implicit_implementation" line INTEGER, -- 1-based line number column_number INTEGER, -- 1-based column number context TEXT, -- trimmed source line @@ -878,13 +878,13 @@ Process exit codes are coarse (`0` success, `1` usage, `2` not-found, `3` db, `4 ## Reference-kind filtering matrix -Different graph entry points walk different `reference_kind` subsets by design. The split mirrors **call graph vs. dependency graph**: `callers`, `callees`, `hotspots`, and `impact`'s BFS layer model the runtime call graph and exclude metadata-only edges (`attribute` / `annotation`); `deps` and `impact`'s heuristic file-level fallback model the compile-time dependency graph and include metadata edges so that `[JsonConverter(typeof(User))]` and `@Inject(User.class)` still surface as real dependencies of `User`. Both directions of `deps` share the same SQL function (`DbReader.GetFileDependencies`), so forward and reverse walks always emit the same kind set. +Different graph entry points walk different `reference_kind` subsets by design. The split mirrors **call graph vs. dependency graph**: `callers`, `callees`, `hotspots`, and `impact`'s BFS layer model the runtime call graph and exclude metadata-only edges (`attribute` / `annotation`), while keeping explicit generic invocation type-argument edges (`generic_type_argument`) such as `Process(value)` because they bind a concrete type into an invoked method; `deps` and `impact`'s heuristic file-level fallback model the compile-time dependency graph and include metadata edges so that `[JsonConverter(typeof(User))]` and `@Inject(User.class)` still surface as real dependencies of `User`. Both directions of `deps` share the same SQL function (`DbReader.GetFileDependencies`), so forward and reverse walks always emit the same kind set. | Entry point | Direction | Reference kinds walked | Backing function | | --- | --- | --- | --- | | `references` (CLI / MCP) | symbol-centric | all `reference_kind` rows; narrowed by `--kind` when provided | `DbReader.GetReferences` | -| `callers` / `callees` (default) | source ↔ container | `('call', 'instantiate', 'subscribe')` (= `CallGraphReferenceKindsSql`); metadata kinds rejected at CLI / MCP `--kind` boundary | `DbReader.GetCallers` / `DbReader.GetCallees` | -| `impact` callers mode | transitive forward (BFS) | `('call', 'instantiate', 'subscribe')` via `GetCallersExact` | `DbReader.GetTransitiveCallers` | +| `callers` / `callees` (default) | source ↔ container | `('augmentation', 'call', 'instantiate', 'generic_type_argument', 'subscribe', 'unsubscribe', 'razor_event_binding', 'friend', 'consumes_hook')` (= `CallGraphReferenceKindsSql`); metadata kinds rejected at CLI / MCP `--kind` boundary | `DbReader.GetCallers` / `DbReader.GetCallees` | +| `impact` callers mode | transitive forward (BFS) | `('augmentation', 'call', 'instantiate', 'generic_type_argument', 'subscribe', 'unsubscribe', 'razor_event_binding', 'friend', 'consumes_hook')` via `GetCallersExact` | `DbReader.GetTransitiveCallers` | | `impact` file-hint fallback | reverse (definition file → dependent files) | all kinds; metadata-only rows gated by `IsMetadataTargetUnambiguous` + structured-type evidence | `DbReader.GetFileDependencyHintsToResolvedType` | | `deps` (default = forward) | source file → target file | all kinds; metadata rows require class-like + metadata-eligible targets (`has_metadata_target_kind`) and a unique resolution (`target_ambiguity`) | `DbReader.GetFileDependencies` | | `deps --reverse` | target file → source file | same as forward `deps` (same SQL) | `DbReader.GetFileDependencies` | @@ -1806,7 +1806,7 @@ symbol_references ( id INTEGER PRIMARY KEY AUTOINCREMENT, file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, symbol_name TEXT, -- 参照先シンボル名 - reference_kind TEXT, -- "call", "instantiate", "subscribe", "razor_event_binding", "attribute", "annotation", "decorator", "type_reference", "implicit_implementation" + reference_kind TEXT, -- "call", "instantiate", "generic_type_argument", "subscribe", "razor_event_binding", "attribute", "annotation", "decorator", "type_reference", "implicit_implementation" line INTEGER, -- 1始まりの行番号 column_number INTEGER, -- 1始まりの列番号 context TEXT, -- trim済みソース行 @@ -2413,13 +2413,13 @@ USER_GUIDEの[終了コード](USER_GUIDE.md#終了コード)セクションを ## reference_kind フィルタの対応表 -グラフ系エントリポイントは、用途別に意図的に異なる `reference_kind` の部分集合だけを辿る。設計上の分割は **呼び出しグラフ vs 依存グラフ** に対応する: `callers`、`callees`、`hotspots`、`impact` の BFS 層は実行時の呼び出し・グラフ可視な結合をモデル化するため metadata 専用エッジ (`attribute` / `annotation`) を除外し、TypeScript merged interface 用の `augmentation` は dependency edge として含める。`deps` と `impact` の heuristic file-level fallback はコンパイル時の依存グラフをモデル化するため、`[JsonConverter(typeof(User))]` や `@Inject(User.class)` も `User` への本物の依存として metadata エッジを含める。`deps` は forward / reverse とも同じ SQL 関数 (`DbReader.GetFileDependencies`) を共有するため、両方向で常に同じ kind 集合を出す。 +グラフ系エントリポイントは、用途別に意図的に異なる `reference_kind` の部分集合だけを辿る。設計上の分割は **呼び出しグラフ vs 依存グラフ** に対応する: `callers`、`callees`、`hotspots`、`impact` の BFS 層は実行時の呼び出し・グラフ可視な結合をモデル化するため metadata 専用エッジ (`attribute` / `annotation`) を除外し、`Process(value)` のように呼び出し済みメソッドへ具体型を束縛する明示的 generic 呼び出し型引数 (`generic_type_argument`) は含める。`deps` と `impact` の heuristic file-level fallback はコンパイル時の依存グラフをモデル化するため、`[JsonConverter(typeof(User))]` や `@Inject(User.class)` も `User` への本物の依存として metadata エッジを含める。`deps` は forward / reverse とも同じ SQL 関数 (`DbReader.GetFileDependencies`) を共有するため、両方向で常に同じ kind 集合を出す。 | エントリポイント | 方向 | 辿る reference_kind | 実装 | | --- | --- | --- | --- | | `references` (CLI / MCP) | symbol 中心 | すべての `reference_kind` 行 (`--kind` 指定時は絞り込み) | `DbReader.GetReferences` | -| `callers` / `callees` (デフォルト) | source ↔ container | `('augmentation', 'call', 'instantiate', 'subscribe', 'unsubscribe', 'razor_event_binding', 'friend', 'consumes_hook')` (= `CallGraphReferenceKindsSql`)。metadata 種別は CLI / MCP `--kind` 境界で拒否 | `DbReader.GetCallers` / `DbReader.GetCallees` | -| `impact` callers mode | 推移的 forward (BFS) | `GetCallersExact` 経由で `('augmentation', 'call', 'instantiate', 'subscribe', 'unsubscribe', 'razor_event_binding', 'friend', 'consumes_hook')` | `DbReader.GetTransitiveCallers` | +| `callers` / `callees` (デフォルト) | source ↔ container | `('augmentation', 'call', 'instantiate', 'generic_type_argument', 'subscribe', 'unsubscribe', 'razor_event_binding', 'friend', 'consumes_hook')` (= `CallGraphReferenceKindsSql`)。metadata 種別は CLI / MCP `--kind` 境界で拒否 | `DbReader.GetCallers` / `DbReader.GetCallees` | +| `impact` callers mode | 推移的 forward (BFS) | `GetCallersExact` 経由で `('augmentation', 'call', 'instantiate', 'generic_type_argument', 'subscribe', 'unsubscribe', 'razor_event_binding', 'friend', 'consumes_hook')` | `DbReader.GetTransitiveCallers` | | `impact` file-hint fallback | reverse (定義ファイル → 依存先) | 全 kind。metadata 専用行は `IsMetadataTargetUnambiguous` と structured-type evidence で gating | `DbReader.GetFileDependencyHintsToResolvedType` | | `deps` (デフォルト = forward) | source file → target file | 全 kind。metadata 行は class-like かつ metadata-eligible な target (`has_metadata_target_kind`) と一意解決 (`target_ambiguity`) を要求 | `DbReader.GetFileDependencies` | | `deps --reverse` | target file → source file | forward `deps` と同じ SQL を共有 | `DbReader.GetFileDependencies` | diff --git a/changelog.d/unreleased/2062.fixed.md b/changelog.d/unreleased/2062.fixed.md new file mode 100644 index 0000000000..8190f45b85 --- /dev/null +++ b/changelog.d/unreleased/2062.fixed.md @@ -0,0 +1,20 @@ +--- +category: fixed +issues: + - 2062 +affected: + - src/CodeIndex/Indexer/References/ReferenceExtractor.TypeReferences.cs + - src/CodeIndex/Database/DbReader.cs + - src/CodeIndex/Database/DbReader.GraphQueries.cs + - DEVELOPER_GUIDE.md + - tests/CodeIndex.Tests/ReferenceExtractorTests.cs + - tests/CodeIndex.Tests/DbReaderTests.cs +--- + +## English + +- **C# generic invocation type arguments now participate in graph queries (#2062)** — explicit calls such as `Process(value)` now add a graph edge for `IFoo`, so callers and impact analysis can follow concrete generic type arguments. + +## 日本語 + +- **C# の generic 呼び出しの型引数が graph query に参加するようになりました (#2062)** — `Process(value)` のような明示的呼び出しで `IFoo` への graph edge を追加し、callers と impact analysis が具体的な generic 型引数を辿れるようにしました。 diff --git a/src/CodeIndex/Database/DbReader.GraphQueries.cs b/src/CodeIndex/Database/DbReader.GraphQueries.cs index 792735651a..06ba02643a 100644 --- a/src/CodeIndex/Database/DbReader.GraphQueries.cs +++ b/src/CodeIndex/Database/DbReader.GraphQueries.cs @@ -694,6 +694,7 @@ private static string ReferenceKindCountSql(string columnSql, string kind) => private static string ReferenceWeightedScoreSql(string columnSql) => $@" SUM(CASE {columnSql} WHEN 'instantiate' THEN 3.0 + WHEN 'generic_type_argument' THEN 0.5 WHEN 'call' THEN 1.0 WHEN 'subscribe' THEN 0.1 ELSE 0.0 @@ -702,7 +703,7 @@ ELSE 0.0 private static string BuildReferenceRankOrderSql(ReferenceRankMode rankMode) => rankMode switch { ReferenceRankMode.Count => "reference_count DESC", - ReferenceRankMode.Kind => "CASE reference_kind WHEN 'instantiate' THEN 0 WHEN 'invoke' THEN 0 WHEN 'call' THEN 1 WHEN 'subscribe' THEN 2 WHEN 'event' THEN 2 ELSE 3 END, reference_count DESC", + ReferenceRankMode.Kind => "CASE reference_kind WHEN 'instantiate' THEN 0 WHEN 'invoke' THEN 0 WHEN 'call' THEN 1 WHEN 'generic_type_argument' THEN 2 WHEN 'subscribe' THEN 3 WHEN 'event' THEN 3 ELSE 4 END, reference_count DESC", _ => "weighted_score DESC, reference_count DESC", }; diff --git a/src/CodeIndex/Database/DbReader.cs b/src/CodeIndex/Database/DbReader.cs index 4595b4d1ca..bc6dc97803 100644 --- a/src/CodeIndex/Database/DbReader.cs +++ b/src/CodeIndex/Database/DbReader.cs @@ -153,11 +153,13 @@ ELSE 0 // Reference kinds that participate in the call-graph (callers/callees/hotspots). Metadata // kinds such as `attribute` / `annotation` are excluded so they do not inflate the graph // with non-call edges (issue #293); React `consumes_hook` and C++ `friend` edges are retained - // because users expect them in dependency-oriented graph queries. + // because users expect them in dependency-oriented graph queries. C# generic type arguments + // are retained only when they are attached to an explicit invocation, so impact can follow + // `Process(x)` without promoting ordinary type annotations to call-graph edges (#2062). // call-graph (callers/callees/hotspots) に参加する reference kind。`attribute` / `annotation` // のようなメタデータ kind は非呼び出しエッジなのでここから除外する (issue #293)。 // Razor の `razor_event_binding`、React の `consumes_hook`、C++ の `friend` は依存関係 graph query に含める。 - internal const string CallGraphReferenceKindsSql = "('augmentation', 'call', 'instantiate', 'subscribe', 'unsubscribe', 'razor_event_binding', 'friend', 'consumes_hook')"; + internal const string CallGraphReferenceKindsSql = "('augmentation', 'call', 'instantiate', 'generic_type_argument', 'subscribe', 'unsubscribe', 'razor_event_binding', 'friend', 'consumes_hook')"; private const string SyntheticTopLevelCallerName = ""; private const string SyntheticTopLevelCallerKind = "function"; diff --git a/src/CodeIndex/Indexer/References/ReferenceExtractor.TypeReferences.cs b/src/CodeIndex/Indexer/References/ReferenceExtractor.TypeReferences.cs index 2acca86091..58e1ddfbb8 100644 --- a/src/CodeIndex/Indexer/References/ReferenceExtractor.TypeReferences.cs +++ b/src/CodeIndex/Indexer/References/ReferenceExtractor.TypeReferences.cs @@ -567,13 +567,14 @@ internal static void AddTypeReferenceSegment( SymbolRecord? container, string language, bool isEscapedCSharpIdentifier = false, - IReadOnlySet? ignoredSegments = null) + IReadOnlySet? ignoredSegments = null, + string referenceKind = "type_reference") { if (segment.Length == 0 || IsIgnoredTypeReferenceSegment(language, segment, isEscapedCSharpIdentifier, ignoredSegments)) return; int column = startInLine + 1; // 1-based / 1始まり - var dedupeKey = BuildReferenceDedupeKey(fileId, language, lineNumber, column, "type_reference", segment); + var dedupeKey = BuildReferenceDedupeKey(fileId, language, lineNumber, column, referenceKind, segment); if (!seen.Add(dedupeKey)) return; @@ -581,7 +582,7 @@ internal static void AddTypeReferenceSegment( { FileId = fileId, SymbolName = segment, - ReferenceKind = "type_reference", + ReferenceKind = referenceKind, Line = lineNumber, Column = column, Context = context, @@ -3357,6 +3358,69 @@ private static void EmitGenericInvocationTypeArgumentReferences( container, language, ignoredSegments); + AddGenericInvocationTypeArgumentSegments( + references, + seen, + fileId, + preparedLine.Substring(argumentsStart, argumentsLength), + argumentsStart, + context, + lineNumber, + container, + language, + ignoredSegments); + } + + private static void AddGenericInvocationTypeArgumentSegments( + List references, + HashSet seen, + long fileId, + string expression, + int expressionStartInLine, + string context, + int lineNumber, + SymbolRecord? container, + string language, + IReadOnlySet? ignoredSegments) + { + if (language != "csharp") + return; + + for (var i = 0; i < expression.Length; i++) + { + if (!IsTypeExpressionIdentifierStart(language, expression[i])) + continue; + + var segmentStart = i; + if (expression[i] == '@') + i++; + while (i < expression.Length && IsTypeExpressionIdentifierPart(language, expression[i])) + i++; + + var rawSegment = expression.Substring(segmentStart, i - segmentStart); + var isEscapedCSharpIdentifier = rawSegment.Length > 0 && rawSegment[0] == '@'; + var segment = NormalizeCSharpIdentifier(rawSegment); + if (i + 1 < expression.Length && expression[i] == ':' && expression[i + 1] == ':') + { + i++; + continue; + } + + AddTypeReferenceSegment( + references, + seen, + fileId, + segment, + expressionStartInLine + segmentStart, + context, + lineNumber, + container, + language, + isEscapedCSharpIdentifier, + ignoredSegments, + "generic_type_argument"); + i--; + } } private static bool TryGetPostNameGenericInvocationTypeArgumentSpan( diff --git a/tests/CodeIndex.Tests/DbReaderTests.cs b/tests/CodeIndex.Tests/DbReaderTests.cs index eb783a00fa..6b250e4dd2 100644 --- a/tests/CodeIndex.Tests/DbReaderTests.cs +++ b/tests/CodeIndex.Tests/DbReaderTests.cs @@ -148,6 +148,32 @@ public void AnalyzeSymbol_KotlinValueClassIncludesSubKind() Assert.Equal("kotlin_value_class", definition.SubKind); } + [Fact] + public void GetCallers_CSharpGenericInvocationTypeArgument_ParticipatesInGraph() + { + InsertIndexedFile( + "src/generic_type_argument_fixture.cs", + "csharp", + """ + interface IFoo {} + class Runner + { + void Process(T item) {} + void Run(IFoo value) { Process(value); } + } + """); + + var caller = Assert.Single(_reader.GetCallers( + "IFoo", + lang: "csharp", + referenceKind: "generic_type_argument", + exact: true, + pathPatterns: ["generic_type_argument_fixture"])); + + Assert.Equal("Run", caller.CallerName); + Assert.Equal("generic_type_argument", caller.ReferenceKind); + } + [Fact] public void CreateSearchReferencesCommand_RanksWithoutLoweringReferenceNames() { diff --git a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs index 0ffc9c4f68..aca8f9654d 100644 --- a/tests/CodeIndex.Tests/ReferenceExtractorTests.cs +++ b/tests/CodeIndex.Tests/ReferenceExtractorTests.cs @@ -83,6 +83,31 @@ public void AddTypeReferenceSegment_DedupesWithinFileAndLanguageOnly() Assert.Contains(references, reference => reference.FileId == 2 && reference.SymbolName == "Runner"); } + [Fact] + public void Extract_CSharpGenericInvocation_EmitsGraphTypeArgumentReference() + { + const string content = """ + interface IFoo {} + class Runner + { + void Process(T item) {} + void Run(IFoo value) { Process(value); } + } + """; + + var symbols = SymbolExtractor.Extract(1, "csharp", content); + var references = ReferenceExtractor.Extract(1, "csharp", content, symbols); + + Assert.Contains(references, reference => + reference.SymbolName == "IFoo" + && reference.ReferenceKind == "type_reference" + && reference.ContainerName == "Run"); + Assert.Contains(references, reference => + reference.SymbolName == "IFoo" + && reference.ReferenceKind == "generic_type_argument" + && reference.ContainerName == "Run"); + } + [Fact] public void Extract_CustomReferencePlugin_HandlesUnsupportedLanguage() { From 8c3df0c2a0b3df0c3377a1c4d134a6afaaa4b0c2 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sun, 24 May 2026 18:46:07 +0900 Subject: [PATCH 2/2] Strengthen generic type argument graph test (#2062) --- tests/CodeIndex.Tests/DbReaderTests.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/CodeIndex.Tests/DbReaderTests.cs b/tests/CodeIndex.Tests/DbReaderTests.cs index 6b250e4dd2..59bce22597 100644 --- a/tests/CodeIndex.Tests/DbReaderTests.cs +++ b/tests/CodeIndex.Tests/DbReaderTests.cs @@ -163,6 +163,15 @@ void Process(T item) {} } """); + var defaultCaller = Assert.Single(_reader.GetCallers( + "IFoo", + lang: "csharp", + exact: true, + pathPatterns: ["generic_type_argument_fixture"])); + + Assert.Equal("Run", defaultCaller.CallerName); + Assert.Equal("generic_type_argument", defaultCaller.ReferenceKind); + var caller = Assert.Single(_reader.GetCallers( "IFoo", lang: "csharp",