From 994bd966f5f0c655c0914ef89d7b667af6536ffe Mon Sep 17 00:00:00 2001 From: Widthdom Date: Wed, 3 Jun 2026 00:46:19 +0900 Subject: [PATCH 1/5] Fix C# graph member conflation (#2818) --- changelog.d/unreleased/2818.fixed.md | 18 ++++++++++ .../Database/DbReader.GraphQueries.cs | 33 +++++++++++++++++ .../Symbols/SymbolExtractor.CSharpScanner.cs | 2 +- tests/CodeIndex.Tests/DbReaderTests.cs | 36 +++++++++++++++++++ tests/CodeIndex.Tests/SymbolExtractorTests.cs | 26 ++++++++++++++ 5 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 changelog.d/unreleased/2818.fixed.md diff --git a/changelog.d/unreleased/2818.fixed.md b/changelog.d/unreleased/2818.fixed.md new file mode 100644 index 0000000000..b121a7ffdc --- /dev/null +++ b/changelog.d/unreleased/2818.fixed.md @@ -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 を除外します。 diff --git a/src/CodeIndex/Database/DbReader.GraphQueries.cs b/src/CodeIndex/Database/DbReader.GraphQueries.cs index 05c962befa..8b65bc0199 100644 --- a/src/CodeIndex/Database/DbReader.GraphQueries.cs +++ b/src/CodeIndex/Database/DbReader.GraphQueries.cs @@ -7,6 +7,35 @@ namespace CodeIndex.Database; public partial class DbReader { + 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 (instr({contextSql}, '.' || {referenceAlias}.symbol_name) > 0 OR ({referenceAlias}.column_number > 1 AND substr({contextSql}, {referenceAlias}.column_number - 1, 1) = '.')))"; + } + + 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 == '_'); + } + /// /// Find callers for a referenced symbol. /// 指定シンボルを呼び出している呼び出し元を探す。 @@ -100,6 +129,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) { @@ -239,6 +269,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"))}"; @@ -325,6 +356,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"))}"; @@ -851,6 +883,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 diff --git a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.CSharpScanner.cs b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.CSharpScanner.cs index a6f616dc75..6e61800f9e 100644 --- a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.CSharpScanner.cs +++ b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.CSharpScanner.cs @@ -1165,7 +1165,7 @@ private static bool HasInvalidCSharpReturnTypeSuffix(string? returnType) return true; var lastChar = trimmed[^1]; - if (lastChar is '<' or '=' or ':' or '+' or '-' or '/' or '%' or '!' or '&' or '|' or '^' or '~' or '.') + if (lastChar is '<' or '=' or ':' or ',' or '+' or '-' or '/' or '%' or '!' or '&' or '|' or '^' or '~' or '.') return true; var tokenStart = trimmed.Length - 1; diff --git a/tests/CodeIndex.Tests/DbReaderTests.cs b/tests/CodeIndex.Tests/DbReaderTests.cs index b10c135795..bb1c54ae4d 100644 --- a/tests/CodeIndex.Tests/DbReaderTests.cs +++ b/tests/CodeIndex.Tests/DbReaderTests.cs @@ -5071,6 +5071,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() { diff --git a/tests/CodeIndex.Tests/SymbolExtractorTests.cs b/tests/CodeIndex.Tests/SymbolExtractorTests.cs index 3590c6dc57..b8670156b0 100644 --- a/tests/CodeIndex.Tests/SymbolExtractorTests.cs +++ b/tests/CodeIndex.Tests/SymbolExtractorTests.cs @@ -186,6 +186,32 @@ public void Extract_CsharpManyMethods_DoesNotRescanMethodBodiesAsFieldCandidates Assert.DoesNotContain(symbols, symbol => symbol.Kind == "function" && symbol.Signature?.Contains("value++", StringComparison.Ordinal) == true); } + [Fact] + public void Extract_CsharpExpressionArgumentMemberCalls_AreNotDefinitions() + { + const string content = """ + using System; + + public class Scanner + { + public object Build() + { + return new ScanFilesResult( + files, + errors, + nonIndexablePaths.ToList(), + unknownExtensionFiles.OrderBy(path => path, StringComparer.Ordinal).ToList(), + Guid.NewGuid().ToString("N")); + } + } + """; + + var symbols = SymbolExtractor.Extract(1, "csharp", content); + + Assert.Contains(symbols, symbol => symbol.Kind == "function" && symbol.Name == "Build"); + Assert.DoesNotContain(symbols, symbol => symbol.Kind == "function" && symbol.Name is "ToList" or "NewGuid" or "ToString"); + } + [Fact] public void Extract_PythonDataclassField_IndexesFieldAndMetadataKeys() { From 6d80fb8669239bb2c44e96dd56395aef544ada5d Mon Sep 17 00:00:00 2001 From: Widthdom Date: Wed, 3 Jun 2026 01:19:15 +0900 Subject: [PATCH 2/5] Scope LSP symbol lookups by document (#2831) --- changelog.d/unreleased/2831.fixed.md | 16 +++ src/CodeIndex/Lsp/LspServer.cs | 56 +++++++-- tests/CodeIndex.Tests/LspServerTests.cs | 160 ++++++++++++++++++++++++ 3 files changed, 223 insertions(+), 9 deletions(-) create mode 100644 changelog.d/unreleased/2831.fixed.md diff --git a/changelog.d/unreleased/2831.fixed.md b/changelog.d/unreleased/2831.fixed.md new file mode 100644 index 0000000000..209640c017 --- /dev/null +++ b/changelog.d/unreleased/2831.fixed.md @@ -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 を混ぜにくくなりました。 diff --git a/src/CodeIndex/Lsp/LspServer.cs b/src/CodeIndex/Lsp/LspServer.cs index a1e8d9fba8..71641eaceb 100644 --- a/src/CodeIndex/Lsp/LspServer.cs +++ b/src/CodeIndex/Lsp/LspServer.cs @@ -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; @@ -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)); @@ -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 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 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"); @@ -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) diff --git a/tests/CodeIndex.Tests/LspServerTests.cs b/tests/CodeIndex.Tests/LspServerTests.cs index 1a69f7f1f1..001f5aaf20 100644 --- a/tests/CodeIndex.Tests/LspServerTests.cs +++ b/tests/CodeIndex.Tests/LspServerTests.cs @@ -292,6 +292,140 @@ public void HandleMessage_Definition_ReturnsLocationForTokenAtPosition() } } + [Fact] + public void HandleMessage_Definition_PrefersCurrentIndexedDocumentForCommonToken() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_lsp_definition_common_token"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + var alphaPath = Path.Combine(projectRoot, "alpha.cs"); + var betaPath = Path.Combine(projectRoot, "beta.cs"); + var alphaSource = """ + class Alpha + { + void Run() { } + void Call() { var alpha = new Alpha(); alpha.Run(); } + } + """; + var betaSource = """ + class Beta + { + void Run() { } + void Call() { var beta = new Beta(); beta.Run(); } + } + """; + File.WriteAllText(alphaPath, alphaSource); + File.WriteAllText(betaPath, betaSource); + TestProjectHelper.InsertIndexedFile(dbPath, "alpha.cs", "csharp", alphaSource); + TestProjectHelper.InsertIndexedFile(dbPath, "beta.cs", "csharp", betaSource); + using var db = new DbContext(dbPath); + using var server = new LspServer(new DbReader(db), "1.2.3", ProgramRunner.CreateDefaultJsonOptions(), projectRoot); + var request = CreateDefinitionRequest(betaPath, 31, 3, CharacterOf(betaSource, 3, "Run();")); + + var response = server.HandleMessage(request); + + Assert.NotNull(response); + var locations = response!["result"]!.AsArray(); + var location = Assert.Single(locations); + Assert.Equal(new Uri(betaPath).AbsoluteUri, location!["uri"]!.GetValue()); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void HandleMessage_References_PrefersCurrentIndexedDocumentForCommonToken() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_lsp_references_common_token"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + var alphaPath = Path.Combine(projectRoot, "alpha.cs"); + var betaPath = Path.Combine(projectRoot, "beta.cs"); + var alphaSource = """ + class Worker { public Worker() { } } + + class Alpha + { + void Call() { var worker = new Worker(); } + } + """; + var betaSource = """ + class Worker { public Worker() { } } + + class Beta + { + void Call() { var worker = new Worker(); } + } + """; + File.WriteAllText(alphaPath, alphaSource); + File.WriteAllText(betaPath, betaSource); + TestProjectHelper.InsertIndexedFile(dbPath, "alpha.cs", "csharp", alphaSource); + TestProjectHelper.InsertIndexedFile(dbPath, "beta.cs", "csharp", betaSource); + MarkGraphReady(dbPath); + using var db = new DbContext(dbPath); + using var server = new LspServer(new DbReader(db), "1.2.3", ProgramRunner.CreateDefaultJsonOptions(), projectRoot); + var request = CreateReferencesRequest(betaPath, 32, 4, CharacterOf(betaSource, 4, "Worker();")); + + var response = server.HandleMessage(request); + + Assert.NotNull(response); + var locations = response!["result"]!.AsArray(); + Assert.NotEmpty(locations); + Assert.All(locations, location => Assert.Equal(new Uri(betaPath).AbsoluteUri, location!["uri"]!.GetValue())); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + + [Fact] + public void HandleMessage_References_PrefersCurrentIndexedDocumentWhenCommonTokenHasNoDefinitions() + { + var projectRoot = TestProjectHelper.CreateTempProject("cdidx_lsp_references_common_token_no_definition"); + try + { + var dbPath = TestProjectHelper.CreateProjectDb(projectRoot); + var alphaPath = Path.Combine(projectRoot, "alpha.cs"); + var betaPath = Path.Combine(projectRoot, "beta.cs"); + var alphaSource = """ + class Alpha + { + void Call() { System.Console.WriteLine("alpha"); } + } + """; + var betaSource = """ + class Beta + { + void Call() { System.Console.WriteLine("beta"); } + } + """; + File.WriteAllText(alphaPath, alphaSource); + File.WriteAllText(betaPath, betaSource); + TestProjectHelper.InsertIndexedFile(dbPath, "alpha.cs", "csharp", alphaSource); + TestProjectHelper.InsertIndexedFile(dbPath, "beta.cs", "csharp", betaSource); + MarkGraphReady(dbPath); + using var db = new DbContext(dbPath); + using var server = new LspServer(new DbReader(db), "1.2.3", ProgramRunner.CreateDefaultJsonOptions(), projectRoot); + var request = CreateReferencesRequest(betaPath, 33, 2, CharacterOf(betaSource, 2, "WriteLine")); + + var response = server.HandleMessage(request); + + Assert.NotNull(response); + var locations = response!["result"]!.AsArray(); + Assert.NotEmpty(locations); + Assert.All(locations, location => Assert.Equal(new Uri(betaPath).AbsoluteUri, location!["uri"]!.GetValue())); + } + finally + { + TestProjectHelper.DeleteDirectory(projectRoot); + } + } + [Fact] public void HandleMessage_Definition_ReturnsEmptyForUnindexedDocument() { @@ -513,4 +647,30 @@ private static string CreateDefinitionRequest(string sourcePath, int id, int lin private static string Frame(string payload) => $"Content-Length: {Encoding.UTF8.GetByteCount(payload)}\r\n\r\n{payload}"; + + private static string CreateReferencesRequest(string sourcePath, int id, int line, int character) => + JsonSerializer.Serialize(new + { + jsonrpc = "2.0", + id, + method = "textDocument/references", + @params = new + { + textDocument = new { uri = new Uri(sourcePath).AbsoluteUri }, + position = new { line, character }, + }, + }); + + private static int CharacterOf(string source, int line, string value) + { + var lines = source.Split('\n'); + return lines[line].IndexOf(value, StringComparison.Ordinal); + } + + private static void MarkGraphReady(string dbPath) + { + using var db = new DbContext(dbPath); + var writer = new DbWriter(db.Connection); + writer.MarkGraphReady(); + } } From de91b1f5685b86014fd9f36eac6536401c6ac5dd Mon Sep 17 00:00:00 2001 From: Widthdom Date: Wed, 3 Jun 2026 04:57:17 +0900 Subject: [PATCH 3/5] Fix C# graph CI regressions (#2818) --- src/CodeIndex/Database/DbReader.GraphQueries.cs | 2 +- src/CodeIndex/Indexer/Symbols/SymbolExtractor.CSharpScanner.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CodeIndex/Database/DbReader.GraphQueries.cs b/src/CodeIndex/Database/DbReader.GraphQueries.cs index 8b65bc0199..fe641480bb 100644 --- a/src/CodeIndex/Database/DbReader.GraphQueries.cs +++ b/src/CodeIndex/Database/DbReader.GraphQueries.cs @@ -18,7 +18,7 @@ private static string BuildCSharpBareMemberGraphReferenceFilter( if (!ShouldFilterCSharpReceiverQualifiedBareMemberQuery(query, lang, exact)) return string.Empty; - return $" AND NOT ({fileAlias}.lang = 'csharp' AND (instr({contextSql}, '.' || {referenceAlias}.symbol_name) > 0 OR ({referenceAlias}.column_number > 1 AND substr({contextSql}, {referenceAlias}.column_number - 1, 1) = '.')))"; + return $" AND NOT ({fileAlias}.lang = 'csharp' AND {referenceAlias}.reference_kind = 'call' AND (instr({contextSql}, '.' || {referenceAlias}.symbol_name) > 0 OR ({referenceAlias}.column_number > 1 AND substr({contextSql}, {referenceAlias}.column_number - 1, 1) = '.')))"; } private static bool ShouldFilterCSharpReceiverQualifiedBareMemberQuery(string query, string? lang, bool exact) diff --git a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.CSharpScanner.cs b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.CSharpScanner.cs index 6e61800f9e..a6f616dc75 100644 --- a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.CSharpScanner.cs +++ b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.CSharpScanner.cs @@ -1165,7 +1165,7 @@ private static bool HasInvalidCSharpReturnTypeSuffix(string? returnType) return true; var lastChar = trimmed[^1]; - if (lastChar is '<' or '=' or ':' or ',' or '+' or '-' or '/' or '%' or '!' or '&' or '|' or '^' or '~' or '.') + if (lastChar is '<' or '=' or ':' or '+' or '-' or '/' or '%' or '!' or '&' or '|' or '^' or '~' or '.') return true; var tokenStart = trimmed.Length - 1; From 87dd77c3d57690d1815e67d906b4513b34433218 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Wed, 3 Jun 2026 09:22:57 +0900 Subject: [PATCH 4/5] Fix C# graph CI regressions follow-up (#2818) --- src/CodeIndex/Database/DbReader.GraphQueries.cs | 2 +- .../Indexer/Symbols/SymbolExtractor.CSharpScanner.cs | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/CodeIndex/Database/DbReader.GraphQueries.cs b/src/CodeIndex/Database/DbReader.GraphQueries.cs index fe641480bb..08ae645078 100644 --- a/src/CodeIndex/Database/DbReader.GraphQueries.cs +++ b/src/CodeIndex/Database/DbReader.GraphQueries.cs @@ -18,7 +18,7 @@ private static string BuildCSharpBareMemberGraphReferenceFilter( 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 ({referenceAlias}.column_number > 1 AND substr({contextSql}, {referenceAlias}.column_number - 1, 1) = '.')))"; + 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) diff --git a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.CSharpScanner.cs b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.CSharpScanner.cs index a6f616dc75..f3fb93e8b9 100644 --- a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.CSharpScanner.cs +++ b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.CSharpScanner.cs @@ -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; @@ -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 symbols, From 0b3d4c4348e7caee4e6337d80074e59a35403fa5 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Wed, 3 Jun 2026 10:27:50 +0900 Subject: [PATCH 5/5] Limit C# receiver graph filter regressions (#2818) --- src/CodeIndex/Database/DbReader.GraphQueries.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/CodeIndex/Database/DbReader.GraphQueries.cs b/src/CodeIndex/Database/DbReader.GraphQueries.cs index d09a8304d6..f663dda838 100644 --- a/src/CodeIndex/Database/DbReader.GraphQueries.cs +++ b/src/CodeIndex/Database/DbReader.GraphQueries.cs @@ -7,6 +7,13 @@ namespace CodeIndex.Database; public partial class DbReader { + private static readonly HashSet CSharpReceiverQualifiedBareMemberFilterNames = new(StringComparer.Ordinal) + { + "GetString", + "ToList", + "NewGuid", + }; + private static string BuildCSharpBareMemberGraphReferenceFilter( string query, string? lang, @@ -33,7 +40,8 @@ private static bool ShouldFilterCSharpReceiverQualifiedBareMemberQuery(string qu if (value.Length == 0 || !(char.IsLetter(value[0]) || value[0] == '_')) return false; - return value.All(c => char.IsLetterOrDigit(c) || c == '_'); + return value.All(c => char.IsLetterOrDigit(c) || c == '_') + && CSharpReceiverQualifiedBareMemberFilterNames.Contains(value); } ///