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
33 changes: 33 additions & 0 deletions DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1052,6 +1052,24 @@ unqualified name receive a global candidate only when that name is unique in the
symbol set. Otherwise they remain `ambiguous` or `unresolved`, and dependency queries do not
fall back to a same-name edge.

C# `type_reference` candidates are filtered before qualifier and namespace ranking. A candidate
must be a type-like symbol (`class`, `struct`, `record`, `interface`, `enum`, or `delegate`), and
when the reference's generic arity can be recovered from its normalized line context, that arity
must match the declaration. Arity recovery skips valid block-comment trivia and treats the original
source column as an upper bound after the persisted context has been trimmed, so a later same-name
generic on the line cannot steal the reference. Non-type name collisions, ordinal-case mismatches,
and mismatched generic declarations never
participate in resolution; if no compatible candidate remains, the reference stays `unresolved`.
An uppercase property receiver such as `Name.Trim()` is rewritten to a qualified member reference
before graph resolution, including when the property is declared in another partial-class file or
an indexed base class. The closest property in the inheritance chain is selected with ordinal name
matching, so the type-only filter does not discard or ambiguously retarget a real member dependency.
This is a general candidate-compatibility rule rather than a framework-type blacklist, and it
does not change Java reference resolution.
The persisted reference-identity contract is versioned for this rule, so indexes written before
the compatibility filter are treated as non-authoritative until a normal index refresh rebuilds
their candidates.

Reference finalization computes candidate count, minimum symbol ID, distinct target-family
count, and stable target key in one correlated aggregate per reference. Keep these four
resolution fields on the row-value assignment path; separate scalar subqueries multiply the
Expand Down Expand Up @@ -4176,6 +4194,21 @@ resolution を再構築し、同じ transaction で marker を設定します。
対象となる symbol 集合で名前が一意の場合だけ global candidate を持ちます。それ以外は
`ambiguous` または `unresolved` のままとし、dependency query は同名 edge へ fallback しません。

C# の `type_reference` candidate は qualifier / namespace の順位付け前に絞り込みます。candidate は
型相当の symbol(`class`、`struct`、`record`、`interface`、`enum`、`delegate`)でなければならず、
正規化済み行 context から reference の generic arity を復元できる場合は declaration の arity と
一致する必要があります。arity の復元では有効な block comment trivia を読み飛ばし、永続化 context の
trim 後は元の source column を上限として扱うため、同じ行の後続同名 generic を誤って選びません。
型ではない同名 symbol、大文字小文字が ordinal 一致しない symbol、arity 不一致の generic declaration は解決候補に
含めず、適合 candidate が残らなければ reference は `unresolved` のままです。これは framework 型の
blacklist ではなく一般的な candidate compatibility rule です。`Name.Trim()` のように大文字で始まる
property receiver は graph 解決前に修飾済み member reference へ書き換え、property が partial class の
別ファイルまたは index 済み base class で宣言されている場合も inheritance chain 上で最も近い property を
ordinal 名一致で選び、型限定 filter によって実在する member dependency を失ったり曖昧化したりしないようにします。
Java の reference resolution は変更しません。
この規則は persisted reference-identity contract の version 対象であり、compatibility filter 導入前に
作成された index は、通常の index 更新で candidate を再構築するまで非 authoritative として扱います。

reference finalization は、candidate count、最小 symbol ID、distinct target-family count、安定 target
key を reference ごとに1回の correlated aggregate で計算します。この4つの resolution field は
row-value assignment のまま維持してください。scalar subquery を分けると、大規模 graph で
Expand Down
4 changes: 4 additions & 0 deletions TESTING_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,8 @@ Use `docs/test-doc-maintenance-plan.md` before moving oversized suites or adding
keeps SCSS variable/mixin/extend references beside SQL multiline MERGE hints, non-ASCII identifiers, quoted-string masking, and temporary-table body boundaries in one multi-file workspace with one CLI index. Use language-specific queries and unique per-file sentinels so count, kind, and line assertions remain independently diagnostic.
- `QueryCommandRunnerTests.RunReferences_ExactJson_CSharpQueryBoundariesAndVisualBasicQuerySyntaxShareIndexedWorkspace`
keeps C# LINQ range-member, parenthesized group-by, awaited keyword-local, nullable suffix, ternary, and keyword-local boundaries beside a Visual Basic query-syntax call in one multi-language workspace with one CLI index. Keep unique per-file sentinels and language-specific queries so zero-reference negatives and positive context, kind, container, and line contracts remain independently diagnostic.
- `QueryCommandRunnerTests.CSharpTypeReferences_ResolveOnlyToTypeLikeSymbolsWithMatchingArity_Issue4825`
indexes one real C# fixture containing user-defined generic classes and delegates, block-comment generic trivia, repeated same-line generic names under indentation, unrelated same-name properties, ordinal-case type/property collisions, `Action<T>` / `Stream` references, and uppercase property receivers split across partial-class and inherited-base files. Keep CLI `references`, `inspect`, and `deps` assertions together with the MCP `references`, `analyze_symbol`, and `deps` wrappers so unresolved diagnostics, generic-arity selection, case-sensitive receiver handling, false dependency-edge suppression, and preservation of real member dependencies cannot drift across surfaces.
- `LspServerTests.cs`
LSP JSON-RPC behavior and indexed editor semantics. Reference parity coverage must compare LSP locations with the matching CLI candidate bundle, including overload identity and both `includeDeclaration` states. Keep document/workspace identifier ranges and explicit-versus-inferred inlay hints in one source-semantics fixture so persisted-column anchoring, source confirmation, and hint suppression cannot drift independently.
Document-symbol hierarchy coverage keeps positional and body record members in one same-line fixture, asserts the complete root-to-member shape and deterministic sibling order, and retains the separate same-range top-level controls so order-independent parent resolution cannot over-nest unrelated symbols. Keep a same-line duplicate-container fixture with distinct positional members so selection-column disambiguation cannot move an earlier member beneath a later same-named container.
Expand Down Expand Up @@ -1382,6 +1384,8 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests"
は SCSS の variable / mixin / extend 参照と、SQL の multiline MERGE hint、非 ASCII identifier、quoted-string masking、temporary-table body boundary を、CLI index 1回の multi-file workspace で共有します。言語別 query と file ごとの固有 sentinel を使い、件数・kind・line assertion の診断性を独立に保ってください。
- `QueryCommandRunnerTests.RunReferences_ExactJson_CSharpQueryBoundariesAndVisualBasicQuerySyntaxShareIndexedWorkspace`
は C# LINQ の range-member、parenthesized group-by、awaited keyword-local、nullable suffix、ternary、keyword-local boundary と Visual Basic query syntax 内の call を、CLI index 1回の multi-language workspace で共有します。file ごとの固有 sentinel と言語別 query を使い、zero-reference negative と positive の context、kind、container、line 契約の診断性を独立に保ってください。
- `QueryCommandRunnerTests.CSharpTypeReferences_ResolveOnlyToTypeLikeSymbolsWithMatchingArity_Issue4825`
は user-defined generic class / delegate、generic 間の block comment trivia、indent 下で同一行に反復する同名 generic、無関係な同名 property、大文字小文字だけ異なる type / property collision、`Action<T>` / `Stream` reference、partial class の別ファイルおよび継承 base file に分かれた大文字始まりの property receiver を含む実 C# fixture を1回 index します。CLI の `references`、`inspect`、`deps` と MCP wrapper の `references`、`analyze_symbol`、`deps` を同じ fixture で検証し、unresolved 診断、generic arity による選択、case-sensitive receiver 判定、偽 dependency edge の抑制、実在する member dependency の維持が surface 間でずれないよう維持してください。
- `LspServerTests.cs`
LSP の JSON-RPC 挙動と indexed editor semantics のテスト。reference parity coverage では、overload identity と `includeDeclaration` の両状態を含め、LSP location と対応する CLI candidate bundle を比較してください。document/workspace の identifier range と explicit/inferred inlay hint は 1 つの source-semantics fixture にまとめ、保存済み column の anchoring、source 上の確認、hint 抑制が別々に drift しないようにします。
document-symbol hierarchy coverage は positional member と body member を同一行の record fixture にまとめ、root から member までの完全な形状と決定的な sibling 順序を assert します。順序非依存の親解決が無関係な symbol を過剰に nest しないよう、same-range top-level の control は独立したまま維持してください。selection column による曖昧性解消で前の member が行内で後にある同名 container の配下へ移動しないよう、異なる positional member を持つ同一行の duplicate-container fixture も維持してください。
Expand Down
27 changes: 27 additions & 0 deletions changelog.d/unreleased/4825.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
category: fixed
issues:
- 4825
affected:
- src/CodeIndex/Database/DbContext.ConnectionFunctions.cs
- src/CodeIndex/Database/DbContext.SchemaMetadata.cs
- src/CodeIndex/Database/DbWriter.ReferenceGraphRefreshScope.cs
- src/CodeIndex/Database/DbWriter.References.cs
- src/CodeIndex/Indexer/CSharpTypeReferenceArity.cs
- src/CodeIndex/Indexer/References/ReferenceExtractor.CoreCallClassification.cs
- src/CodeIndex/Indexer/References/ReferenceExtractor.CoreExtraction.cs
- src/CodeIndex/Indexer/References/ReferenceExtractor.CoreLookups.cs
- tests/CodeIndex.Tests/DatabaseTests.cs
- tests/CodeIndex.Tests/DbReaderSymbolIdentityTests.cs
- tests/CodeIndex.Tests/QueryCommandRunnerIssue4825Tests.cs
- DEVELOPER_GUIDE.md
- TESTING_GUIDE.md
---

## English

- **C# type references now resolve only to compatible type-like symbols (#4825)** — C# `type_reference` rows now require an exact-case type-like target with matching generic arity, including generic delegates whose return type shares the delegate name, block-comment generic trivia, and repeated same-line names in trimmed contexts. Framework types such as `Action<T>` and `Stream` no longer bind to unrelated same-name properties or create false dependency edges. References without a compatible indexed type remain unresolved, uppercase property receivers retain exact member dependencies across partial-class and inherited-base files, and indexes written under the prior reference-identity contract are treated as non-authoritative until refreshed.

## 日本語

- **C# の型参照を互換性のある型相当 symbol だけに解決するようにしました (#4825)** — C# の `type_reference` row は大文字小文字が正確に一致する型相当 target と一致する generic arity を必須とし、戻り値型が delegate 名と同名の generic delegate、generic 間の block comment trivia、trim 済み context 内で同一行に反復する名前も正しく扱うようになりました。`Action<T>` や `Stream` などの framework 型が無関係な同名 property に結び付いて偽の dependency edge を作らなくなり、適合する index 済み型がない reference は unresolved のままです。大文字で始まる property receiver の実在する member dependency は partial class の別ファイル間と継承 base file の双方で ordinal 一致を維持し、以前の reference-identity contract で作成された index は更新されるまで非 authoritative として扱います。
116 changes: 116 additions & 0 deletions src/CodeIndex/Database/DbContext.ConnectionFunctions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Microsoft.Data.Sqlite;
using System.Globalization;
using System.Runtime.ExceptionServices;
using System.Text.Json;

namespace CodeIndex.Database;

Expand Down Expand Up @@ -70,6 +71,42 @@ internal static void RegisterConnectionFunctions(SqliteConnection connection)
connection.CreateFunction(
"csharp_identifier_occurrence_count",
(string? text, string? identifier) => CountCSharpIdentifierOccurrences(text, identifier));
connection.CreateFunction(
"csharp_reference_type_arity",
(string? context, string? identifier, long? columnNumber) =>
CSharpTypeReferenceArity.GetReferenceArity(context, identifier, columnNumber));
connection.CreateFunction(
"csharp_reference_is_member_receiver",
(string? context, string? identifier, long? columnNumber) =>
CSharpTypeReferenceArity.IsMemberReceiver(context, identifier, columnNumber));
connection.CreateFunction(
"csharp_definition_type_arity",
(string? signature, string? identifier, string? symbolKind) =>
CSharpTypeReferenceArity.GetDefinitionArity(signature, identifier, symbolKind));
connection.CreateFunction(
"csharp_base_identifiers_json",
(string? signature) =>
JsonSerializer.Serialize(
DbWriter.ParseCSharpBaseIdentifiers(signature),
CliJsonSerializerContext.Default.ListString));
connection.CreateFunction(
"csharp_base_name_folded",
(string? baseReference) =>
{
var leaf = GetCSharpBaseReferenceLeaf(baseReference);
return leaf == null ? null : NameFold.Fold(leaf) ?? leaf;
});
connection.CreateFunction(
"csharp_base_name",
(string? baseReference) => GetCSharpBaseReferenceLeaf(baseReference));
connection.CreateFunction(
"csharp_base_reference_matches",
(string? baseReference, string? candidateName, string? candidateQualifiedName, string? derivingQualifiedName) =>
CSharpBaseReferenceMatches(
baseReference,
candidateName,
candidateQualifiedName,
derivingQualifiedName) ? 1 : 0);
connection.CreateFunction(
"sql_normalize_exact_source_name",
(string? text, string? lang) => string.IsNullOrWhiteSpace(text) ? null : ExactSourceSearchNormalizer.Normalize(text, lang));
Expand Down Expand Up @@ -170,6 +207,85 @@ internal static int CountCSharpIdentifierOccurrences(string? text, string? ident
return count;
}

private static bool CSharpBaseReferenceMatches(
string? baseReference,
string? candidateName,
string? candidateQualifiedName,
string? derivingQualifiedName)
{
if (string.IsNullOrWhiteSpace(baseReference)
|| string.IsNullOrWhiteSpace(candidateName)
|| string.IsNullOrWhiteSpace(candidateQualifiedName)
|| string.IsNullOrWhiteSpace(derivingQualifiedName))
{
return false;
}

var normalizedReference =
CSharpVerbatimNameNormalizer.Normalize(baseReference.Trim());
if (normalizedReference.StartsWith("global::", StringComparison.Ordinal))
normalizedReference = normalizedReference["global::".Length..];

var normalizedCandidateName =
CSharpVerbatimNameNormalizer.Normalize(candidateName.Trim());
var normalizedCandidateQualifiedName =
CSharpVerbatimNameNormalizer.Normalize(candidateQualifiedName.Trim());
if (normalizedReference.Contains('.', StringComparison.Ordinal)
|| normalizedReference.Contains("::", StringComparison.Ordinal))
{
return string.Equals(
normalizedReference.Replace("::", ".", StringComparison.Ordinal),
normalizedCandidateQualifiedName,
StringComparison.Ordinal);
}

if (!string.Equals(
normalizedReference,
normalizedCandidateName,
StringComparison.Ordinal))
{
return false;
}

var derivingScope = GetQualifiedNameScope(
CSharpVerbatimNameNormalizer.Normalize(derivingQualifiedName.Trim()));
var candidateScope = GetQualifiedNameScope(normalizedCandidateQualifiedName);
while (true)
{
if (string.Equals(
derivingScope,
candidateScope,
StringComparison.Ordinal))
{
return true;
}

if (derivingScope.Length == 0)
return false;
derivingScope = GetQualifiedNameScope(derivingScope);
}
}

private static string? GetCSharpBaseReferenceLeaf(string? baseReference)
{
if (string.IsNullOrWhiteSpace(baseReference))
return null;

var normalized =
CSharpVerbatimNameNormalizer.Normalize(baseReference.Trim());
if (normalized.StartsWith("global::", StringComparison.Ordinal))
normalized = normalized["global::".Length..];
normalized = normalized.Replace("::", ".", StringComparison.Ordinal);
var lastDot = normalized.LastIndexOf('.');
return lastDot < 0 ? normalized : normalized[(lastDot + 1)..];
}

private static string GetQualifiedNameScope(string qualifiedName)
{
var lastDot = qualifiedName.LastIndexOf('.');
return lastDot < 0 ? string.Empty : qualifiedName[..lastDot];
}

internal static bool HasCSharpIdentifierOccurrenceOutsideLineRange(string? text, string? identifier, int startLine, int endLine)
{
if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(identifier))
Expand Down
9 changes: 8 additions & 1 deletion src/CodeIndex/Database/DbContext.SchemaMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,14 @@ public static string BuildIncompleteHotspotFamilyMarkerFingerprint(string? finge
public const string SqlGraphContractVersionMetaKey = "sql_graph_contract_version";
public const int HdlGraphContractVersion = 1;
public const string HdlGraphContractVersionMetaKey = "hdl_graph_contract_version";
public const int ReferenceIdentityContractVersion = 2;
// Version 3 (#4825) invalidates persisted reference candidates written before C#
// type references were constrained to type-like symbols with compatible generic arity.
// Version 2 DBs can otherwise keep authoritative-looking property bindings and false
// dependency edges until an explicit index refresh.
// バージョン 3 (#4825) では、C# の型参照を型相当 symbol と互換 generic arity に限定する
// 前に永続化された reference candidate を無効化する。バージョン 2 の DB をそのまま信頼すると、
// 明示的な index 更新まで property への誤結合と偽 dependency edge が残るためである。
public const int ReferenceIdentityContractVersion = 3;
public const string ReferenceIdentityContractVersionMetaKey = "reference_identity_contract_version";
public static string GetDynamicReferenceGraphContractVersionMetaKey(string lang) =>
$"dynamic_reference_graph_contract_version_{lang}";
Expand Down
Loading
Loading