Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
5f76687
Preserve C# explicit-interface symbol identity (#4866)
Widthdom Jul 28, 2026
4d65046
Merge remote-tracking branch 'origin/main' into fix-issue4866
Widthdom Jul 28, 2026
66882fc
Harden explicit-interface fold reconstruction (#4866)
Widthdom Jul 28, 2026
449dcfc
Merge remote-tracking branch 'origin/main' into fix-issue4866
Widthdom Jul 28, 2026
7ec8338
Address explicit-interface identity review findings (#4866)
Widthdom Jul 29, 2026
c2d0192
Merge remote-tracking branch 'origin/main' into fix-issue4866
Widthdom Jul 29, 2026
0d47bd3
Fix explicit-interface review edge cases (#4866)
Widthdom Jul 29, 2026
54de221
Keep explicit-interface aliases indexed (#4866)
Widthdom Jul 29, 2026
5857c09
Scope C# alias backfill to identity rows (#4866)
Widthdom Jul 29, 2026
acf8d3e
Merge remote-tracking branch 'origin/main' into fix-issue4866
Widthdom Jul 29, 2026
22ac98b
Constrain explicit-interface fold reconstruction (#4866)
Widthdom Jul 29, 2026
fe81951
Merge remote-tracking branch 'origin/main' into fix-issue4866
Widthdom Jul 29, 2026
e28ddaa
Harden explicit-interface upgrade readiness (#4866)
Widthdom Jul 29, 2026
2b629eb
Fix final explicit-interface review regressions (#4866)
Widthdom Jul 29, 2026
e9b6274
Merge remote-tracking branch 'origin/main' into fix-issue4866
Widthdom Jul 29, 2026
bb94b60
Harden explicit-interface backfill compatibility (#4866)
Widthdom Jul 29, 2026
2656027
Decode explicit-interface Unicode escapes (#4866)
Widthdom Jul 29, 2026
55b7e69
Merge remote-tracking branch 'origin/main' into fix-issue4866
Widthdom Jul 29, 2026
c80c0a2
Fix explicit-interface review regressions (#4866)
Widthdom Jul 29, 2026
b8aad69
Fix explicit-interface final review findings (#4866)
Widthdom Jul 29, 2026
ac7a59f
Preserve future C# contract metadata (#4866)
Widthdom Jul 29, 2026
c3755d8
Merge remote-tracking branch 'origin/main' into fix-issue4866
Widthdom Jul 29, 2026
cbe346d
Distinguish stale C# folds from missing aliases (#4866)
Widthdom Jul 29, 2026
46bd713
Merge remote-tracking branch 'origin/main' into fix-issue4866
Widthdom Jul 29, 2026
6feeb90
Resolve explicit C# identities in impact lookup (#4866)
Widthdom Jul 29, 2026
f3b454b
Fix adversarial C# identity edge cases (#4866)
Widthdom Jul 29, 2026
4e1fbc6
Merge remote-tracking branch 'origin/main' into fix-issue4866
Widthdom Jul 29, 2026
22c1157
Merge remote-tracking branch 'origin/main' into fix-issue4866
Widthdom Jul 29, 2026
dd7ba8c
Handle missing impact root identity in boundary probes (#4866)
Widthdom Jul 29, 2026
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
31 changes: 29 additions & 2 deletions DEVELOPER_GUIDE.md

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,15 @@ cdidx license --json
cdidx validate
```

For C# explicit-interface implementations, symbol output and outlines keep the
short display name (`Run`, `Value`, `Changed`, or `Item`). Qualified exact-name
queries use the interface spelling, such as `IFoo.Run<T>`, `IFoo.Value`,
`IFoo.Changed`, and `IFoo.Item` (`IFoo.this` is accepted as an indexer alias).
Generic parameter spelling is normalized to arity, so `IFoo.Run<TValue>` has the
same exact identity as `IFoo.Run<T>`. An unqualified exact query such as `Run`
remains a discovery alias and may return explicit implementations alongside
same-named public members; use the qualified spelling to select one identity.

`doctor --env-inventory=full` accepts exact `--env-domain`,
`--env-category`, and `--env-sensitivity` filters that compose with AND.
Its JSON form accepts `--max-json-bytes` as a whole-document UTF-8 byte cap;
Expand Down Expand Up @@ -527,6 +536,14 @@ cdidx license --json
cdidx validate
```

C# の明示的 interface 実装では、symbol 出力と outline は短い表示名
(`Run`、`Value`、`Changed`、`Item`)を維持します。修飾した exact-name query には
`IFoo.Run<T>`、`IFoo.Value`、`IFoo.Changed`、`IFoo.Item` のような interface 表記を使い、
indexer では `IFoo.this` も alias として受け付けます。generic parameter の表記は arity に
正規化されるため、`IFoo.Run<TValue>` と `IFoo.Run<T>` は同じ完全一致 identity です。
`Run` のような非修飾の完全一致 query は discovery alias のままで、同名の public member と
明示的実装をともに返す場合があります。1つの identity を選ぶには修飾表記を使ってください。

`doctor --env-inventory=full` は、AND で合成される完全一致の
`--env-domain`、`--env-category`、`--env-sensitivity` filter を受け付けます。
JSON 形式では `--max-json-bytes` を文書全体の UTF-8 byte cap として利用できるため、
Expand Down
2 changes: 2 additions & 0 deletions TESTING_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ Use `docs/test-doc-maintenance-plan.md` before moving oversized suites or adding
Extractor coverage is split by language or feature area with partial test classes, while shared helpers remain on the root `SymbolExtractorTests` / `ReferenceExtractorTests` parts.
YAML sequence-hierarchy fixtures must keep sequence items path-only, count empty/scalar/alias items in stable indexes, preserve anchor and block-scalar boundaries, assert logical qualified paths separately from nearest emitted symbol parents, and prove a normal full scan refreshes unchanged YAML rows stamped with the prior extractor contract.
C# declaration-boundary regressions should pair a direct extractor fixture with a real-index `symbols --exact-name` query. Keep invocation and parameter continuations beside valid multi-line methods, constructors, delegates, and local functions so both false-positive rejection and declaration ranges remain observable.
C# explicit-interface identity coverage pairs extractor assertions for methods, properties, events, and indexers with persisted exact qualified/unqualified queries, fold rewrite validation, inspect/outline checks, and LSP definition/reference scoping. Include multiple and inherited interfaces, generic arity, a same-named public member, and a qualified return type that must not be mistaken for an explicit-interface qualifier.
C# callable-containment fixtures should cover block-bodied test methods, local and nested local functions, named lambdas, expression-bodied members, and nested types together, asserting both symbol parents and call-reference containers.
Repository-metadata coverage lives in `SymbolExtractorRepositoryMetadataTests.cs` and `ReferenceExtractorRepositoryMetadataTests.cs`; keep TOML, JSON Lines, ignore/attributes, EditorConfig, `.rules`, and application-manifest capability assertions coordinated with conservative local-path and malformed-record controls.
Capability-regression fixtures that require an unsupported language use the explicit `text` placeholder or an ambiguity bucket; do not use a recognized repository-metadata format as the unsupported control.
Expand Down Expand Up @@ -1010,6 +1011,7 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests"
extractor のカバレッジは言語または機能領域ごとの partial test class に分割し、共有 helper は root 側の `SymbolExtractorTests` / `ReferenceExtractorTests` に残します。
YAML sequence hierarchy の fixture では、sequence item を path 専用のまま保ち、empty / scalar / alias item を安定した index に数え、anchor と block scalar の境界を維持し、論理 qualified path と最寄りの出力済み symbol parent を分けて検証し、旧 extractor contract が stamp された未変更 YAML row を通常の full scan が更新することも確認してください。
C# の declaration boundary に関する regression では、extractor を直接呼ぶ fixture と、実 index に対する `symbols --exact-name` query を組み合わせてください。呼び出し・parameter の continuation と、正当な複数行 method、constructor、delegate、local function を同居させ、false positive の拒否と宣言 range の両方を観測可能にします。
C# の明示的 interface identity coverage では、method、property、event、indexer の extractor assertion と、永続化後の修飾 / 非修飾の完全一致 query、fold rewrite 検証、inspect / outline、LSP の definition / reference scope を組み合わせます。複数および継承 interface、generic arity、同名 public member に加え、明示的 interface qualifier と誤認してはならない修飾 return type を含めてください。
C# の callable containment fixture では、block body の test method、local / nested local function、named lambda、expression-bodied member、nested type を同居させ、symbol の親と call reference の container の両方を検証してください。
repository metadata の coverage は `SymbolExtractorRepositoryMetadataTests.cs` と `ReferenceExtractorRepositoryMetadataTests.cs` に置きます。TOML、JSON Lines、ignore / attributes、EditorConfig、`.rules`、application manifest の capability assertion を、保守的な local-path 抽出と malformed-record control に同期させてください。
未対応言語を必要とする capability regression fixture には明示的な `text` placeholder または ambiguity bucket を使い、認識済み repository metadata 形式を未対応 control に使わないでください。
Expand Down
20 changes: 20 additions & 0 deletions changelog.d/unreleased/4866.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
category: fixed
issues:
- 4866
affected:
- src/CodeIndex/Database
- src/CodeIndex/Indexer
- tests/CodeIndex.Tests
- README.md
- DEVELOPER_GUIDE.md
- TESTING_GUIDE.md
---

## English

- **C# explicit-interface members now retain distinct symbol identities (#4866)** — Exact qualified queries preserve the interface qualifier and generic arity for methods, properties, events, and indexers without merging them with same-named public members, including indexer aliases when no language filter is supplied. Short display names remain available through a separately indexed Unicode-folded discovery alias across CLI, inspect, outline, and LSP navigation. `backfill-fold` upgrades the previous C# naming contract without reparsing when persisted signatures are available; legacy databases without that reconstruction evidence remain unready and request a C# refresh instead of receiving a false v3 readiness stamp.

## 日本語

- **C# の明示的 interface member が個別の symbol identity を保持するようになりました (#4866)** — method、property、event、indexer の完全一致 query で interface qualifier と generic arity を保持し、言語 filter 未指定時の indexer alias を含めて同名の public member と統合しません。CLI、inspect、outline、LSP navigation では、短い表示名を別途 index 化した Unicode-folded discovery alias として引き続き利用できます。`backfill-fold` は永続 signature が残る場合に source の再解析なしで以前の C# naming contract を更新し、復元根拠がない legacy DB には誤った v3 readiness stamp を付けず C# refresh を案内します。
6 changes: 6 additions & 0 deletions src/CodeIndex/Cli/DiffCommandRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ private static DiffJsonResult CompareDatabasesCore(
"sub_kind",
"name",
"name_folded",
"display_name_folded",
"line",
"start_line",
"start_column",
Expand Down Expand Up @@ -1301,6 +1302,9 @@ private static string BuildSymbolRowsSql(SqliteConnection connection)
var metadataTargetSourceExpr = ColumnExists(connection, "symbols", "metadata_target_source")
? "symbols.metadata_target_source"
: "NULL";
var displayNameFoldedExpr = ColumnExists(connection, "symbols", "display_name_folded")
? "symbols.display_name_folded"
: "NULL";

return $$"""
SELECT
Expand All @@ -1309,6 +1313,7 @@ private static string BuildSymbolRowsSql(SqliteConnection connection)
symbols.sub_kind,
symbols.name,
symbols.name_folded,
{{displayNameFoldedExpr}},
symbols.line,
symbols.start_line,
symbols.start_column,
Expand All @@ -1332,6 +1337,7 @@ ORDER BY
symbols.sub_kind,
symbols.name,
symbols.name_folded,
{{displayNameFoldedExpr}},
symbols.line,
symbols.start_line,
symbols.start_column,
Expand Down
31 changes: 30 additions & 1 deletion src/CodeIndex/Cli/IndexCommandRunner.Maintenance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,16 @@ internal static int RunBackfillFold(
if (!options.DryRun)
db.InitializeSchema();
var writer = new DbWriter(db);
if (writer.TryGetNewerCSharpSymbolNameContractVersion(out var newerCSharpContract))
{
return WriteCommandError(
options.Json,
jsonOptions,
$"C# symbol-name contract version {newerCSharpContract} is newer than supported version {DbContext.CSharpSymbolNameContractVersion}",
CommandExitCodes.DatabaseError,
"Use the same or a newer CodeIndex version that wrote this database; this version will not rewrite or downgrade its C# identities.",
CommandErrorCodes.DbError);
}

var userVersionBefore = db.GetUserVersion();
var foldReadyBefore = (userVersionBefore & DbContext.FoldReadyFlag) != 0;
Expand All @@ -579,11 +589,29 @@ internal static int RunBackfillFold(
var storedFoldFingerprint = db.GetMetaString("fold_key_fingerprint");
var foldMetadataCurrentBefore = storedFoldVersion == currentFoldVersion
&& storedFoldFingerprint == currentFoldFingerprint;
var csharpSymbolNameContractUpgradeRequired =
writer.HasAnyFilesWithLanguage("csharp")
&& !string.Equals(
db.GetMetaString(DbContext.CSharpSymbolNameContractVersionMetaKey),
DbContext.CSharpSymbolNameContractVersion.ToString(
System.Globalization.CultureInfo.InvariantCulture),
StringComparison.Ordinal);
if (csharpSymbolNameContractUpgradeRequired
&& !writer.CanReconstructCSharpExplicitInterfaceIdentitiesFromPersistedRows())
{
return WriteCommandError(
options.Json,
jsonOptions,
"C# explicit-interface identities cannot be reconstructed because legacy symbol signatures are missing",
CommandExitCodes.DatabaseError,
"Refresh the C# files with `cdidx index <projectPath>` (or rebuild the index) before retrying `cdidx backfill-fold`.",
CommandErrorCodes.DbError);
}
foldReadyBefore = foldReadyBefore && foldMetadataCurrentBefore;
// Missing or mismatched fold metadata means persisted keys may have been generated
// by a different fold algorithm/runtime, so refresh every row from source names.
// fold metadata 未記録 / 不一致時は全行再計算して version/runtime skew を解消する。
var rewriteAll = !foldMetadataCurrentBefore;
var rewriteAll = writer.ResolveFoldBackfillRewriteAll(!foldMetadataCurrentBefore);

var symbols = 0;
var symbolReferences = 0;
Expand Down Expand Up @@ -617,6 +645,7 @@ internal static int RunBackfillFold(
"Retry `cdidx backfill-fold`. If the DB still does not verify, rebuild it with `cdidx index <projectPath> --rebuild`.",
CommandErrorCodes.DbError);
}
writer.MarkCSharpSymbolNameContractReady();

transaction.Commit();
userVersionAfter = db.GetUserVersion();
Expand Down
4 changes: 4 additions & 0 deletions src/CodeIndex/Database/DbContext.ConnectionFunctions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ internal static void RegisterConnectionFunctions(SqliteConnection connection)
var leafName = SqlNameResolver.GetLeafName(name);
return leafName.Length == 0 ? null : NameFold.Fold(leafName) ?? leafName;
});
connection.CreateFunction(
"codeindex_name_fold",
(string? name) => NameFold.Fold(name),
isDeterministic: true);
connection.CreateFunction(
"sql_normalize_name",
(string? name) => string.IsNullOrWhiteSpace(name) ? null : SqlNameResolver.NormalizeQualifiedName(name));
Expand Down
3 changes: 3 additions & 0 deletions src/CodeIndex/Database/DbContext.ReadMigrations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -255,10 +255,13 @@ PRIMARY KEY(reference_id, symbol_id)
// not fail on legacy DBs where the column did not exist yet.
// #86: folded 列を追加してから folded index を作らないと legacy DB でクラッシュする。
yield return ("EnsureColumn symbols.name_folded", () => EnsureColumn("symbols", "name_folded", "TEXT"));
yield return ("EnsureColumn symbols.display_name_folded", () => EnsureColumn("symbols", "display_name_folded", "TEXT"));
yield return ("EnsureColumn symbol_references.symbol_name_folded", () => EnsureColumn("symbol_references", "symbol_name_folded", "TEXT"));
yield return ("EnsureColumn symbol_references.container_name_folded", () => EnsureColumn("symbol_references", "container_name_folded", "TEXT"));
yield return ("CREATE INDEX idx_symbols_name_folded",
() => Execute("CREATE INDEX IF NOT EXISTS idx_symbols_name_folded ON symbols(name_folded)"));
yield return ("CREATE INDEX idx_symbols_display_name_folded",
() => Execute("CREATE INDEX IF NOT EXISTS idx_symbols_display_name_folded ON symbols(display_name_folded) WHERE display_name_folded IS NOT NULL"));
yield return ("CREATE INDEX idx_symbols_file_name_folded",
() => Execute("CREATE INDEX IF NOT EXISTS idx_symbols_file_name_folded ON symbols(file_id, name_folded)"));
yield return ("CREATE INDEX idx_symbols_file_name_nocase",
Expand Down
20 changes: 15 additions & 5 deletions src/CodeIndex/Database/DbContext.SchemaInitialization.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,9 @@ container_kind TEXT CHECK (container_kind IS NULL OR container_kind IN (" + sym
visibility TEXT,
return_type TEXT,
is_metadata_target INTEGER,
metadata_target_source TEXT
metadata_target_source TEXT,
name_folded TEXT,
display_name_folded TEXT
)");

// Indexed references table / 参照インデックステーブル
Expand Down Expand Up @@ -228,6 +230,7 @@ private void MigrateCoreTableColumns()
// COLLATE NOCASE path (correct for ASCII, misses non-ASCII casing — #86 fix).
// #86: --exact 用の Unicode 折り畳み列。レガシー行は NULL のまま、再 index で埋まる。
EnsureColumn("symbols", "name_folded", "TEXT");
EnsureColumn("symbols", "display_name_folded", "TEXT");
EnsureColumn("symbol_references", "symbol_name_folded", "TEXT");
EnsureColumn("symbol_references", "container_name_folded", "TEXT");
EnsureColumn("symbol_references", "is_self_reference", "INTEGER NOT NULL DEFAULT 0");
Expand Down Expand Up @@ -315,6 +318,11 @@ private void CreateCoreSchemaIndexes()
// the NOCASE indexes above. Both sets coexist so mixed-state DBs cannot regress.
// #86: 折り畳み列のインデックス。FoldReadyFlag が立っている DB でだけ使う。
Execute("CREATE INDEX IF NOT EXISTS idx_symbols_name_folded ON symbols(name_folded)");
// Explicit-interface identities occupy name_folded, while unqualified discovery uses
// the separately persisted display-name fold. Both predicates stay indexed.
// 明示的 interface identity は name_folded、非修飾 discovery は別途永続化した
// display-name fold を使い、両方の predicate を index 対応に保つ。
Execute("CREATE INDEX IF NOT EXISTS idx_symbols_display_name_folded ON symbols(display_name_folded) WHERE display_name_folded IS NOT NULL");
// Reference-source and ranked-candidate resolution repeatedly combines the folded
// symbol name with file or container scope. Keep those probes bounded for every
// indexed language, including the NOCASE fallback used by partially migrated DBs.
Expand Down Expand Up @@ -432,10 +440,11 @@ container_kind TEXT CHECK (container_kind IS NULL OR container_kind IN ({symbol
return_type TEXT,
is_metadata_target INTEGER,
metadata_target_source TEXT,
name_folded TEXT
name_folded TEXT,
display_name_folded TEXT
)
""",
"id, file_id, kind, sub_kind, name, line, start_line, start_column, end_line, body_start_line, body_end_line, signature, container_kind, container_name, container_qualified_name, family_key, visibility, return_type, is_metadata_target, metadata_target_source, name_folded");
"id, file_id, kind, sub_kind, name, line, start_line, start_column, end_line, body_start_line, body_end_line, signature, container_kind, container_name, container_qualified_name, family_key, visibility, return_type, is_metadata_target, metadata_target_source, name_folded, display_name_folded");
RebuildReferenceLineTablesWithRequiredFileId();
RebuildTableWithRequiredFileId(
"file_issues",
Expand Down Expand Up @@ -726,10 +735,11 @@ container_kind TEXT CHECK (container_kind IS NULL OR container_kind IN ({symbol
return_type TEXT,
is_metadata_target INTEGER,
metadata_target_source TEXT,
name_folded TEXT
name_folded TEXT,
display_name_folded TEXT
)
""";
const string symbolsColumns = "id, file_id, kind, sub_kind, name, line, start_line, start_column, end_line, body_start_line, body_end_line, signature, container_kind, container_name, container_qualified_name, family_key, visibility, return_type, is_metadata_target, metadata_target_source, name_folded";
const string symbolsColumns = "id, file_id, kind, sub_kind, name, line, start_line, start_column, end_line, body_start_line, body_end_line, signature, container_kind, container_name, container_qualified_name, family_key, visibility, return_type, is_metadata_target, metadata_target_source, name_folded, display_name_folded";
var symbolReferencesCreateSql =
$"""
CREATE TABLE symbol_references (
Expand Down
Loading
Loading