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

## English

- **C# graph queries no longer conflate common member calls across receivers (#2818)** - member calls such as `paths.ToList()` and `Guid.NewGuid()` inside argument expressions are no longer indexed as standalone function definitions, and bare C# caller queries skip receiver-qualified calls such as `reader.GetString()`.

## 日本語

- **C# graph query が receiver の異なる common member call を混同しないようになりました (#2818)** - 引数式の中にある `paths.ToList()` や `Guid.NewGuid()` のような member call を独立した function definition として index せず、裸の C# caller query では `reader.GetString()` のような receiver-qualified call を除外します。
16 changes: 16 additions & 0 deletions changelog.d/unreleased/2831.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: fixed
issues:
- 2831
affected:
- src/CodeIndex/Lsp/LspServer.cs
- tests/CodeIndex.Tests/LspServerTests.cs
---

## English

- **LSP definition and references now prefer the current indexed document for common tokens (#2831)** - `textDocument/definition` and `textDocument/references` use the resolved document path before falling back to workspace-wide exact matches, avoiding unrelated same-named symbols in other files.

## 日本語

- **LSP definition / references が common token で現在の indexed document を優先するようになりました (#2831)** - `textDocument/definition` と `textDocument/references` は workspace 全体の exact match に fallback する前に解決済み document path を使うため、別ファイルの同名 symbol を混ぜにくくなりました。
41 changes: 41 additions & 0 deletions src/CodeIndex/Database/DbReader.GraphQueries.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,43 @@ namespace CodeIndex.Database;

public partial class DbReader
{
private static readonly HashSet<string> CSharpReceiverQualifiedBareMemberFilterNames = new(StringComparer.Ordinal)
{
"GetString",
"ToList",
"NewGuid",
};

private static string BuildCSharpBareMemberGraphReferenceFilter(
string query,
string? lang,
bool exact,
string contextSql,
string fileAlias,
string referenceAlias)
{
if (!ShouldFilterCSharpReceiverQualifiedBareMemberQuery(query, lang, exact))
return string.Empty;

return $" AND NOT ({fileAlias}.lang = 'csharp' AND {referenceAlias}.reference_kind = 'call' AND (instr({contextSql}, '.' || {referenceAlias}.symbol_name || '(') > 0 OR instr({contextSql}, '.' || {referenceAlias}.symbol_name || '<') > 0 OR ({referenceAlias}.column_number > 1 AND substr({contextSql}, {referenceAlias}.column_number - 1, 1) = '.' AND substr(ltrim(substr({contextSql}, {referenceAlias}.column_number + length({referenceAlias}.symbol_name))), 1, 1) IN ('(', '<'))))";
}

private static bool ShouldFilterCSharpReceiverQualifiedBareMemberQuery(string query, string? lang, bool exact)
{
if (!exact || lang is not null and not "csharp" || SqlNameResolver.HasQualifier(query))
return false;

if (string.IsNullOrWhiteSpace(query))
return false;

var value = query[0] == '@' ? query[1..] : query;
if (value.Length == 0 || !(char.IsLetter(value[0]) || value[0] == '_'))
return false;

return value.All(c => char.IsLetterOrDigit(c) || c == '_')
&& CSharpReceiverQualifiedBareMemberFilterNames.Contains(value);
}

/// <summary>
/// Find callers for a referenced symbol.
/// 指定シンボルを呼び出している呼び出し元を探す。
Expand Down Expand Up @@ -123,6 +160,7 @@ FROM symbol_references r
: " AND (r.symbol_name LIKE @query ESCAPE '\\' OR (f.lang = 'sql' AND r.symbol_name = sql_leaf_name(@aliasQuery) COLLATE NOCASE))";
if (lang != null)
sql += " AND f.lang = @lang";
sql += BuildCSharpBareMemberGraphReferenceFilter(query, lang, exact, contextSql, "f", "r");
AppendPathFilters(ref sql, pathPatterns, excludePathPatterns, excludeTests);
if (referenceKind == null)
{
Expand Down Expand Up @@ -286,6 +324,7 @@ FROM symbol_references r
: " AND (r.symbol_name LIKE @query ESCAPE '\\' OR (f.lang = 'sql' AND r.symbol_name = sql_leaf_name(@aliasQuery) COLLATE NOCASE))";
if (lang != null)
groupedSql += " AND f.lang = @lang";
groupedSql += BuildCSharpBareMemberGraphReferenceFilter(query, lang, exact, contextSql, "f", "r");
AppendPathFilters(ref groupedSql, pathPatterns, excludePathPatterns, excludeTests);
if (referenceKind == null)
groupedSql += $" GROUP BY f.path, f.lang, r.container_kind, r.container_name, r.symbol_name, r.file_id, r.line, r.column_number, {(rawKinds ? GetRawReferenceKindSql("r.reference_kind") : GetLogicalReferenceKindSql("r.reference_kind"))}";
Expand Down Expand Up @@ -396,6 +435,7 @@ FROM symbol_references r
: " AND (r.symbol_name LIKE @query ESCAPE '\\' OR (f.lang = 'sql' AND r.symbol_name = sql_leaf_name(@aliasQuery) COLLATE NOCASE))";
if (lang != null)
groupedSql += " AND f.lang = @lang";
groupedSql += BuildCSharpBareMemberGraphReferenceFilter(query, lang, exact, contextSql, "f", "r");
AppendPathFilters(ref groupedSql, pathPatterns, excludePathPatterns, excludeTests);
if (referenceKind == null)
groupedSql += $" GROUP BY f.path, f.lang, r.container_kind, r.container_name, r.symbol_name, r.file_id, r.line, r.column_number, {(rawKinds ? GetRawReferenceKindSql("r.reference_kind") : GetLogicalReferenceKindSql("r.reference_kind"))}";
Expand Down Expand Up @@ -980,6 +1020,7 @@ AND r.reference_kind IN {CallGraphReferenceKindsSql}
{nameCondition}";
if (lang != null)
sql += " AND f.lang = @lang";
sql += BuildCSharpBareMemberGraphReferenceFilter(symbolName, lang, exact: true, contextSql, "f", "r");
AppendPathFilters(ref sql, pathPatterns, excludePathPatterns, excludeTests);
sql += @"
GROUP BY f.path, f.lang, r.container_kind, r.container_name, r.symbol_name, r.reference_kind, r.file_id, r.line, r.column_number
Expand Down
11 changes: 11 additions & 0 deletions src/CodeIndex/Indexer/Symbols/SymbolExtractor.CSharpScanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1165,6 +1165,9 @@ private static bool HasInvalidCSharpReturnTypeSuffix(string? returnType)
return true;

var lastChar = trimmed[^1];
if (lastChar == ',' && !LooksLikeCSharpDeclaratorListReturnType(trimmed))
return true;

if (lastChar is '<' or '=' or ':' or '+' or '-' or '/' or '%' or '!' or '&' or '|' or '^' or '~' or '.')
return true;

Expand All @@ -1186,6 +1189,14 @@ private static bool HasInvalidCSharpReturnTypeSuffix(string? returnType)
return lastToken is "as" or "is" or "return" or "throw" or "new";
}

private static bool LooksLikeCSharpDeclaratorListReturnType(string returnType)
{
var withoutTrailingComma = returnType[..^1].TrimEnd();
var firstSegmentEnd = withoutTrailingComma.IndexOf(',');
var firstSegment = (firstSegmentEnd >= 0 ? withoutTrailingComma[..firstSegmentEnd] : withoutTrailingComma).Trim();
return firstSegment.Any(char.IsWhiteSpace);
}

private static bool IsInsidePreviouslyEmittedCSharpMemberBody(
string[] lines,
List<SymbolRecord> symbols,
Expand Down
56 changes: 47 additions & 9 deletions src/CodeIndex/Lsp/LspServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ internal sealed class LspServer : IDisposable
private bool _exitRequested;
private bool _exitRequestedBeforeShutdown;

private readonly record struct PositionTokenContext(string Token, string IndexedPath);

public LspServer(DbReader reader, string version, JsonSerializerOptions jsonOptions, string? projectRoot = null)
{
_reader = reader;
Expand Down Expand Up @@ -153,11 +155,11 @@ private JsonArray DocumentSymbol(JsonElement root)

private JsonArray Definition(JsonElement root)
{
var query = ExtractPositionToken(root);
if (string.IsNullOrWhiteSpace(query))
var context = ExtractPositionToken(root);
if (context == null)
return [];

var definitions = _reader.GetDefinitions(query, DefaultLimit, exact: true);
var definitions = ResolveLspDefinitions(context.Value);
var array = new JsonArray();
foreach (var definition in definitions)
array.Add(ToLocation(definition.Path, definition.StartLine, 1, definition.EndLine, 1));
Expand All @@ -166,18 +168,53 @@ private JsonArray Definition(JsonElement root)

private JsonArray References(JsonElement root)
{
var query = ExtractPositionToken(root);
if (string.IsNullOrWhiteSpace(query))
var context = ExtractPositionToken(root);
if (context == null)
return [];

var analysis = _reader.AnalyzeSymbol(query, DefaultLimit, exact: true);
var analysis = ResolveLspReferences(context.Value);
var array = new JsonArray();
foreach (var reference in analysis.References)
array.Add(ToLocation(reference.Path, reference.Line, Math.Max(reference.Column, 1), reference.Line, Math.Max(reference.Column, 1) + Math.Max(query.Length, 1)));
array.Add(ToLocation(reference.Path, reference.Line, Math.Max(reference.Column, 1), reference.Line, Math.Max(reference.Column, 1) + Math.Max(context.Value.Token.Length, 1)));
return array;
}

private string? ExtractPositionToken(JsonElement root)
private List<DefinitionResult> ResolveLspDefinitions(PositionTokenContext context)
{
var localDefinitions = _reader.GetDefinitions(context.Token, DefaultLimit, exact: true, pathPatterns: [context.IndexedPath]);
if (localDefinitions.Count > 0)
return localDefinitions;

var workspaceDefinitions = _reader.GetDefinitions(context.Token, DefaultLimit, exact: true);
return HasSingleLspDefinitionTarget(workspaceDefinitions) ? workspaceDefinitions : [];
}

private SymbolAnalysisResult ResolveLspReferences(PositionTokenContext context)
{
var localDefinitions = _reader.GetDefinitions(context.Token, DefaultLimit, exact: true, pathPatterns: [context.IndexedPath]);
if (localDefinitions.Count > 0)
return _reader.AnalyzeSymbol(context.Token, DefaultLimit, pathPatterns: [context.IndexedPath], exact: true);

var workspaceDefinitions = _reader.GetDefinitions(context.Token, DefaultLimit, exact: true);
if (workspaceDefinitions.Count == 0 || !HasSingleLspDefinitionTarget(workspaceDefinitions))
return _reader.AnalyzeSymbol(context.Token, DefaultLimit, pathPatterns: [context.IndexedPath], exact: true);

return _reader.AnalyzeSymbol(context.Token, DefaultLimit, exact: true);
}

private static bool HasSingleLspDefinitionTarget(IReadOnlyList<DefinitionResult> definitions)
{
if (definitions.Count <= 1)
return true;

var firstKey = BuildLspDefinitionTargetKey(definitions[0]);
return definitions.Skip(1).All(definition => string.Equals(BuildLspDefinitionTargetKey(definition), firstKey, StringComparison.Ordinal));
}

private static string BuildLspDefinitionTargetKey(DefinitionResult definition)
=> string.Join('\0', definition.Path, definition.Kind, definition.ContainerKind, definition.ContainerName, definition.Name);

private PositionTokenContext? ExtractPositionToken(JsonElement root)
{
var path = GetDocumentPath(root);
var line = GetInt32(root, "params", "position", "line");
Expand All @@ -198,7 +235,8 @@ private JsonArray References(JsonElement root)
if (!TryReadPositionLine(indexedFullPath, line, out var sourceLine))
return null;

return ExtractTokenAtUtf16Position(sourceLine, character);
var token = ExtractTokenAtUtf16Position(sourceLine, character);
return string.IsNullOrWhiteSpace(token) ? null : new PositionTokenContext(token, indexedPath);
}

private static bool TryReadPositionLine(string path, int targetLine, out string sourceLine)
Expand Down
36 changes: 36 additions & 0 deletions tests/CodeIndex.Tests/DbReaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5312,6 +5312,42 @@ public void Run()
Assert.Equal(1, dependency.ReferenceCount);
}

[Fact]
public void GraphQueries_CsharpBareMemberCallersSkipReceiverQualifiedCalls()
{
InsertIndexedFile("src/common_member_graph_fixture.cs", "csharp",
"""
using System.Text.Json;

public class Caller
{
private string GetString() => "";

public void Run(LocalApi api, JsonElement json)
{
api.GetString();
json.GetString();
GetString();
}
}

public class LocalApi
{
public string GetString() => "";
}
""");

var callers = _reader.GetCallers("GetString", lang: "csharp", exact: true, pathPatterns: ["common_member_graph_fixture"]);

var caller = Assert.Single(callers);
Assert.Equal("Run", caller.CallerName);
Assert.Equal(1, caller.ReferenceCount);
Assert.Equal(1, _reader.CountCallers("GetString", lang: "csharp", exact: true, pathPatterns: ["common_member_graph_fixture"]));
var total = _reader.CountCallersTotal("GetString", lang: "csharp", exact: true, pathPatterns: ["common_member_graph_fixture"]);
Assert.Equal(1, total.Count);
Assert.Equal(1, total.FileCount);
}

[Fact]
public void SqlQualifiedNames_AlignGraphReadersHotspotsAndUnused()
{
Expand Down
Loading
Loading