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
17 changes: 17 additions & 0 deletions changelog.d/unreleased/2060.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
category: fixed
issues:
- 2060
affected:
- src/CodeIndex/Database/DbReader.CSharpResolution.cs
- src/CodeIndex/Database/DbReader.GraphQueries.cs
- tests/CodeIndex.Tests/DbReaderTests.cs
---

## English

- **C# caller and impact queries now follow polymorphic dispatch to concrete implementations (#2060)** — exact graph traversal expands implementation method queries through inherited abstract base methods and implemented interface methods, so callers of the static base/interface target are reachable from the concrete override.

## 日本語

- **C# の callers / impact が polymorphic dispatch を具象実装まで追跡するようになりました (#2060)** — exact graph traversal で実装メソッドの検索時に継承元の abstract base method と実装 interface method へ展開し、静的な base / interface target を呼ぶ caller が具象 override から到達可能になります。
211 changes: 205 additions & 6 deletions src/CodeIndex/Database/DbReader.CSharpResolution.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,133 @@ private HashSet<string> GetInheritedCSharpContainingTypes(CSharpContainingTypeSc
return inheritedContainingTypes;
}

private HashSet<string> GetPolymorphicCSharpContainingTypes(CSharpContainingTypeScope containingTypeScope)
{
var inheritedContainingTypes = new HashSet<string>(StringComparer.Ordinal);
var visited = new HashSet<string>(StringComparer.Ordinal)
{
containingTypeScope.QualifiedName,
};
CollectPolymorphicCSharpContainingTypes(containingTypeScope, inheritedContainingTypes, visited);
return inheritedContainingTypes;
}

private List<string> GetCSharpPolymorphicDispatchSymbolNames(string symbolName)
{
var memberName = SqlNameResolver.GetLeafName(symbolName);
if (string.IsNullOrWhiteSpace(memberName))
return [];

var containingTypeNames = new List<string>();
var lastDot = symbolName.LastIndexOf('.');
var hasExplicitContainingType = lastDot > 0;
if (hasExplicitContainingType)
{
var explicitContainingTypeName = symbolName[..lastDot];
containingTypeNames.Add(explicitContainingTypeName);
}
else
{
using var cmd = _conn.CreateCommand();
cmd.CommandText = @"
SELECT s.container_qualified_name
FROM symbols s
JOIN files f ON s.file_id = f.id
WHERE f.lang = 'csharp'
AND s.kind IN ('function', 'property')
AND s.container_qualified_name IS NOT NULL
AND s.container_qualified_name != ''
AND s.name = @memberName COLLATE NOCASE
GROUP BY s.container_qualified_name";
cmd.Parameters.AddWithValue("@memberName", memberName);

using var reader = cmd.ExecuteTrackedReader();
while (reader.TrackedRead())
containingTypeNames.Add(reader.GetString(0));
}

var names = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var containingTypeName in containingTypeNames)
{
AddCSharpBaseListDispatchNames(containingTypeName, memberName, names);
}

foreach (var containingTypeName in containingTypeNames)
{
var containingTypeScope = GetCSharpContainingTypeScope(containingTypeName);
if (containingTypeScope == null)
continue;

foreach (var inheritedContainingType in GetPolymorphicCSharpContainingTypes(containingTypeScope))
{
var inheritedMemberName = CombineDbQualifiedName(inheritedContainingType, memberName);
if (!string.IsNullOrWhiteSpace(inheritedMemberName))
names.Add(inheritedMemberName);
if (!hasExplicitContainingType)
names.Add(memberName);
}
}

return names.ToList();
}

private void AddCSharpBaseListDispatchNames(string containingTypeName, string memberName, HashSet<string> names)
{
var signature = GetCSharpContainingTypeScope(containingTypeName)?.Signature;
if (!string.IsNullOrWhiteSpace(signature))
{
AddCSharpBaseListDispatchNamesFromSignature(containingTypeName, memberName, signature, names);
return;
}

var shortTypeName = GetLastQualifiedSegment(containingTypeName);
if (string.IsNullOrWhiteSpace(shortTypeName))
return;

using var cmd = _conn.CreateCommand();
cmd.CommandText = @"
SELECT s.signature
FROM symbols s
JOIN files f ON s.file_id = f.id
WHERE f.lang = 'csharp'
AND s.kind IN ('class', 'struct', 'interface')
AND (s.name = @shortTypeName COLLATE NOCASE OR s.name = @containingTypeName COLLATE NOCASE)
LIMIT 1";
cmd.Parameters.AddWithValue("@shortTypeName", shortTypeName);
cmd.Parameters.AddWithValue("@containingTypeName", containingTypeName);

signature = cmd.ExecuteScalar() as string;
if (string.IsNullOrWhiteSpace(signature))
return;

AddCSharpBaseListDispatchNamesFromSignature(containingTypeName, memberName, signature, names);
}

private static void AddCSharpBaseListDispatchNamesFromSignature(string containingTypeName, string memberName, string signature, HashSet<string> names)
{
var namespacePrefix = string.Empty;
var lastDot = containingTypeName.LastIndexOf('.');
if (lastDot > 0)
namespacePrefix = containingTypeName[..lastDot];

foreach (var baseTypeReference in ParseCSharpBaseTypeReferences(signature))
{
var normalizedBase = NormalizeCSharpBaseTypeReference(baseTypeReference);
if (string.IsNullOrWhiteSpace(normalizedBase))
continue;

var inheritedMemberName = CombineDbQualifiedName(normalizedBase, memberName);
if (!string.IsNullOrWhiteSpace(inheritedMemberName))
names.Add(inheritedMemberName);
if (!string.IsNullOrWhiteSpace(namespacePrefix) && !SqlNameResolver.HasQualifier(normalizedBase))
{
inheritedMemberName = CombineDbQualifiedName(CombineDbQualifiedName(namespacePrefix, normalizedBase), memberName);
if (!string.IsNullOrWhiteSpace(inheritedMemberName))
names.Add(inheritedMemberName);
}
}
}

private void CollectInheritedCSharpContainingTypes(CSharpContainingTypeScope containingTypeScope, HashSet<string> inheritedContainingTypes, HashSet<string> visited)
{
var directBaseScope = ResolveDirectCSharpBaseContainingTypeScope(containingTypeScope);
Expand All @@ -168,6 +295,53 @@ private void CollectInheritedCSharpContainingTypes(CSharpContainingTypeScope con
CollectInheritedCSharpContainingTypes(directBaseScope, inheritedContainingTypes, visited);
}

private void CollectPolymorphicCSharpContainingTypes(CSharpContainingTypeScope containingTypeScope, HashSet<string> inheritedContainingTypes, HashSet<string> visited)
{
foreach (var inheritedScope in ResolveDirectCSharpInheritedContainingTypeScopes(containingTypeScope))
{
if (!visited.Add(inheritedScope.QualifiedName))
continue;

inheritedContainingTypes.Add(inheritedScope.QualifiedName);
CollectPolymorphicCSharpContainingTypes(inheritedScope, inheritedContainingTypes, visited);
}
}

private List<CSharpContainingTypeScope> ResolveDirectCSharpInheritedContainingTypeScopes(CSharpContainingTypeScope containingTypeScope)
{
if (containingTypeScope.Kind is not ("class" or "struct" or "interface"))
return [];

var baseTypeReferences = ParseCSharpBaseTypeReferences(containingTypeScope.Signature);
if (baseTypeReferences.Count == 0)
return [];

var scopes = new List<CSharpContainingTypeScope>();
foreach (var baseTypeReference in baseTypeReferences)
{
var inheritedQualifiedName = ResolveScopedCSharpContainingTypeQualifiedName(
containingTypeScope.Path,
containingTypeScope.DeclarationLine,
baseTypeReference);
if (string.IsNullOrWhiteSpace(inheritedQualifiedName))
continue;

var inheritedScope = GetCSharpContainingTypeScope(inheritedQualifiedName);
if (inheritedScope == null)
continue;
if (containingTypeScope.Kind == "class" && inheritedScope.Kind is not ("class" or "interface"))
continue;
if (containingTypeScope.Kind == "struct" && inheritedScope.Kind != "interface")
continue;
if (containingTypeScope.Kind == "interface" && inheritedScope.Kind != "interface")
continue;

scopes.Add(inheritedScope);
}

return scopes;
}

private CSharpContainingTypeScope? GetCSharpContainingTypeScope(string qualifiedName)
{
if (_csharpContainingTypeScopeByQualifiedName.TryGetValue(qualifiedName, out var cached))
Expand Down Expand Up @@ -651,25 +825,38 @@ AND s.kind IN ('class', 'struct', 'interface')
}

private static string? ParseCSharpBaseTypeReference(string? signature)
{
var references = ParseCSharpBaseTypeReferences(signature);
return references.Count == 0 ? null : references[0];
}

private static List<string> ParseCSharpBaseTypeReferences(string? signature)
{
if (string.IsNullOrWhiteSpace(signature))
return null;
return [];

var text = signature.TrimEnd();
if (text.EndsWith("{", StringComparison.Ordinal))
text = text[..^1].TrimEnd();

var colonIndex = FindCSharpBaseListColonIndex(text);
if (colonIndex < 0)
return null;
return [];

var baseList = text[(colonIndex + 1)..];
var whereIndex = baseList.IndexOf(" where ", StringComparison.Ordinal);
if (whereIndex >= 0)
baseList = baseList[..whereIndex];

var firstEntry = TakeFirstCSharpBaseListEntry(baseList).Trim();
return firstEntry.Length == 0 ? null : firstEntry;
var entries = new List<string>();
foreach (var entry in EnumerateCSharpBaseListEntries(baseList))
{
var trimmed = entry.Trim();
if (trimmed.Length > 0)
entries.Add(trimmed);
}

return entries;
}

private static int FindCSharpBaseListColonIndex(string signature)
Expand Down Expand Up @@ -713,10 +900,19 @@ private static int FindCSharpBaseListColonIndex(string signature)
}

private static string TakeFirstCSharpBaseListEntry(string baseList)
{
foreach (var entry in EnumerateCSharpBaseListEntries(baseList))
return entry;

return baseList;
}

private static IEnumerable<string> EnumerateCSharpBaseListEntries(string baseList)
{
var angleDepth = 0;
var parenDepth = 0;
var squareDepth = 0;
var start = 0;
for (var i = 0; i < baseList.Length; i++)
{
switch (baseList[i])
Expand Down Expand Up @@ -744,12 +940,15 @@ private static string TakeFirstCSharpBaseListEntry(string baseList)
break;
case ',':
if (angleDepth == 0 && parenDepth == 0 && squareDepth == 0)
return baseList[..i];
{
yield return baseList[start..i];
start = i + 1;
}
break;
}
}

return baseList;
yield return baseList[start..];
}

private static string NormalizeCSharpBaseTypeReference(string typeReference)
Expand Down
23 changes: 19 additions & 4 deletions src/CodeIndex/Database/DbReader.GraphQueries.cs
Original file line number Diff line number Diff line change
Expand Up @@ -798,17 +798,25 @@ private List<CallerResult> GetCallersExact(string symbolName, int limit, int off
// caller 側も leaf `--exact` と同じく FoldReady なら folded equality、legacy DB では
// `COLLATE NOCASE` fallback。definition と caller 行の casing 差もここで吸収する。
var allowSqlLeafFallback = !SqlNameResolver.HasQualifier(symbolName);
var polymorphicCSharpSymbolNames = lang is null or "csharp"
? GetCSharpPolymorphicDispatchSymbolNames(symbolName)
: [];
var polymorphicNameCondition = polymorphicCSharpSymbolNames.Count == 0
? string.Empty
: _foldReady
? " OR (f.lang = 'csharp' AND r.symbol_name_folded IN (" + string.Join(", ", polymorphicCSharpSymbolNames.Select((_, i) => $"@polymorphicSymbolNameFolded{i}")) + "))"
: " OR (f.lang = 'csharp' AND r.symbol_name COLLATE NOCASE IN (" + string.Join(", ", polymorphicCSharpSymbolNames.Select((_, i) => $"@polymorphicSymbolName{i}")) + "))";
var nameCondition = _foldReady
? allowSqlLeafFallback
? @"
AND (r.symbol_name_folded = @symbolNameFolded OR (f.lang = 'sql' AND r.symbol_name_folded = @symbolNameLeafFolded))"
AND (r.symbol_name_folded = @symbolNameFolded OR (f.lang = 'sql' AND r.symbol_name_folded = @symbolNameLeafFolded)" + polymorphicNameCondition + ")"
: @"
AND (((f.lang = 'sql') AND sql_context_has_name_folded_at(" + contextSql + @", @symbolName, r.column_number) = 1) OR ((f.lang != 'sql') AND r.symbol_name_folded = @symbolNameFolded))"
AND (((f.lang = 'sql') AND sql_context_has_name_folded_at(" + contextSql + @", @symbolName, r.column_number) = 1) OR ((f.lang != 'sql') AND r.symbol_name_folded = @symbolNameFolded)" + polymorphicNameCondition + ")"
: allowSqlLeafFallback
? @"
AND (r.symbol_name = @symbolName COLLATE NOCASE OR (f.lang = 'sql' AND r.symbol_name = sql_leaf_name(@symbolName) COLLATE NOCASE))"
AND (r.symbol_name = @symbolName COLLATE NOCASE OR (f.lang = 'sql' AND r.symbol_name = sql_leaf_name(@symbolName) COLLATE NOCASE)" + polymorphicNameCondition + ")"
: @"
AND (((f.lang = 'sql') AND sql_context_has_name_at(" + contextSql + @", @symbolName, r.column_number) = 1) OR ((f.lang != 'sql') AND r.symbol_name = @symbolName COLLATE NOCASE))";
AND (((f.lang = 'sql') AND sql_context_has_name_at(" + contextSql + @", @symbolName, r.column_number) = 1) OR ((f.lang != 'sql') AND r.symbol_name = @symbolName COLLATE NOCASE)" + polymorphicNameCondition + ")";

// impact BFS must share the call-graph contract with `callers`/`callees`/`hotspots`,
// so event subscriptions (`Click += OnClick`) also participate in the transitive
Expand Down Expand Up @@ -848,6 +856,13 @@ FROM logical_references r
cmd.Parameters.AddWithValue("@symbolNameLeafFolded", NameFold.Fold(SqlNameResolver.GetLeafName(symbolName)) ?? SqlNameResolver.GetLeafName(symbolName));
if (_foldReady)
cmd.Parameters.AddWithValue("@symbolNameFolded", NameFold.Fold(symbolName) ?? symbolName);
for (var i = 0; i < polymorphicCSharpSymbolNames.Count; i++)
{
if (_foldReady)
cmd.Parameters.AddWithValue($"@polymorphicSymbolNameFolded{i}", NameFold.Fold(polymorphicCSharpSymbolNames[i]) ?? polymorphicCSharpSymbolNames[i]);
else
cmd.Parameters.AddWithValue($"@polymorphicSymbolName{i}", polymorphicCSharpSymbolNames[i]);
}
if (lang != null)
cmd.Parameters.AddWithValue("@lang", lang);
AddPathFilterParameters(cmd, pathPatterns, excludePathPatterns);
Expand Down
Loading
Loading