Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<IFoo>(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` |
Expand Down Expand Up @@ -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済みソース行
Expand Down Expand Up @@ -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<IFoo>(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` |
Expand Down
20 changes: 20 additions & 0 deletions changelog.d/unreleased/2062.fixed.md
Original file line number Diff line number Diff line change
@@ -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<IFoo>(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<IFoo>(value)` のような明示的呼び出しで `IFoo` への graph edge を追加し、callers と impact analysis が具体的な generic 型引数を辿れるようにしました。
3 changes: 2 additions & 1 deletion src/CodeIndex/Database/DbReader.GraphQueries.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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",
};

Expand Down
6 changes: 4 additions & 2 deletions src/CodeIndex/Database/DbReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<IFoo>(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 = "<top-level>";
private const string SyntheticTopLevelCallerKind = "function";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -567,21 +567,22 @@ internal static void AddTypeReferenceSegment(
SymbolRecord? container,
string language,
bool isEscapedCSharpIdentifier = false,
IReadOnlySet<string>? ignoredSegments = null)
IReadOnlySet<string>? 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;

references.Add(new ReferenceRecord
{
FileId = fileId,
SymbolName = segment,
ReferenceKind = "type_reference",
ReferenceKind = referenceKind,
Line = lineNumber,
Column = column,
Context = context,
Expand Down Expand Up @@ -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<ReferenceRecord> references,
HashSet<string> seen,
long fileId,
string expression,
int expressionStartInLine,
string context,
int lineNumber,
SymbolRecord? container,
string language,
IReadOnlySet<string>? 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(
Expand Down
35 changes: 35 additions & 0 deletions tests/CodeIndex.Tests/DbReaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,41 @@ 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>(T item) {}
void Run(IFoo value) { Process<IFoo>(value); }
}
""");

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",
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()
{
Expand Down
25 changes: 25 additions & 0 deletions tests/CodeIndex.Tests/ReferenceExtractorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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>(T item) {}
void Run(IFoo value) { Process<IFoo>(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()
{
Expand Down
Loading