Skip to content

Commit 0f8e2a4

Browse files
authored
Fix C# static readonly tuple field extraction (#4691)
* Fix C# static readonly tuple fields (#4616) * Refresh stale C# indexes for tuple fields (#4616)
1 parent 2ff4c7e commit 0f8e2a4

6 files changed

Lines changed: 123 additions & 4 deletions

File tree

DEVELOPER_GUIDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,7 @@ Do not add mutable static caches, shared `StringBuilder` instances, reused `Matc
473473
| `environment` | Dockerfile `ENV` variable names | Variable/search symbol; participates in Dockerfile variable references |
474474
| `event` | Event declarations | Search/filter symbol |
475475
| `expose` | Dockerfile `EXPOSE` ports | Container runtime search symbol |
476-
| `field` | Field declarations where distinct from properties | Search/filter symbol |
476+
| `field` | Field declarations where distinct from properties; C# const and static readonly fields share the tuple-aware type grammar used by ordinary fields | Search/filter symbol |
477477
| `file_module` | File-scoped module/package declarations | Namespace-like context symbol |
478478
| `function` | Functions, methods, constructors, delegates, tasks, and callable bindings that do not have a narrower kind | Primary callable definition; participates in callers/callees through reference rows |
479479
| `generator` | JavaScript/TypeScript generator declarations | Callable definition; participates in callers/callees through reference rows |
@@ -3330,7 +3330,7 @@ filter、downstream JSON consumer が同じ値を理解できるようにして
33303330
| `environment` | Dockerfile `ENV` variable name | variable/search symbol。Dockerfile variable reference に参加 |
33313331
| `event` | event declaration | Search/filter symbol |
33323332
| `expose` | Dockerfile `EXPOSE` port | container runtime search symbol |
3333-
| `field` | property と区別される field declaration | Search/filter symbol |
3333+
| `field` | property と区別される field declaration。C# の const / static readonly field は通常 field と tuple-aware な型文法を共有する | Search/filter symbol |
33343334
| `file_module` | file-scoped module / package declaration | Namespace-like context symbol |
33353335
| `function` | 関数、method、constructor、delegate、task、およびより狭い kind がない callable binding | Primary callable definition。reference row 経由で callers/callees に参加 |
33363336
| `generator` | JavaScript / TypeScript generator declaration | Callable definition。reference row 経由で callers/callees に参加 |

TESTING_GUIDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ Use `docs/test-doc-maintenance-plan.md` before moving oversized suites or adding
197197
C# query-range collision forms for basic selection, directional ordering, keyword-named members, and object initializers share one indexed source when they assert the same empty inspect reference bundle.
198198
C# inspect brace-range regressions place char-literal, raw-string, and verbatim-string forms in one indexed class and query each following method from that shared fixture.
199199
C# generic query-range selectors share simple and tuple type arguments in one fixture, while generic type-pattern coverage shares designation and no-designation forms in another fixture.
200+
C# field-type grammar coverage shares static readonly tuple, nullable-tuple, generic-tuple-member, const/plain-field controls, and deconstruction negatives in one extractor fixture. The full-scan regression also seeds the previous C# extractor contract against an unchanged file and verifies that incremental indexing restores the tuple field kind and complete return type.
200201
C# switch-expression pattern-variable coverage keeps recursive, declaration, guard, and comment-trivia forms in one extractor fixture when their contract is the set of genuine enum-member references.
201202
C# foreach-shadowing coverage shares embedded, same-line, and dangling-else forms in one source and distinguishes the surviving references by container.
202203
C# lambda-parameter shadowing coverage shares simple, multiline, after-lambda, and same-line boundary forms in one extraction pass.
@@ -912,6 +913,7 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests"
912913
C# query-range collision の basic selection、directional ordering、keyword 名 member、object initializer は、同じ空の inspect reference bundle を検証する場合は1つの indexed source を共有してください。
913914
C# inspect の brace-range regression は char literal、raw string、verbatim string を1つの indexed class に併置し、それぞれの後続 method を共有 fixture から query してください。
914915
C# generic query-range selector は単純型引数と tuple 型引数を1つの fixture で共有し、generic type-pattern coverage は designation 有無を別の1 fixture で共有してください。
916+
C# field 型文法 coverage は static readonly tuple、nullable tuple、generic 内 tuple member、const / 通常 field の control、deconstruction の負例を1つの extractor fixture で共有してください。full-scan regression では未変更 file に以前の C# extractor contract を設定し、incremental index が tuple field の kind と完全な return type を復元することも検証してください。
915917
C# switch-expression pattern-variable coverage は、真の enum-member reference 集合を同じ contract とする recursive、declaration、guard、comment-trivia 形式を1つの extractor fixture で共有してください。
916918
C# foreach shadowing coverage は embedded、same-line、dangling-else 形式を1つの source で共有し、残る reference を container で区別してください。
917919
C# lambda parameter shadowing coverage は simple、multiline、after-lambda、same-line boundary 形式を1回の extraction pass で共有してください。
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
category: fixed
3+
issues:
4+
- 4616
5+
affected:
6+
- src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs
7+
- tests/CodeIndex.Tests/SymbolExtractorCSharpTests.cs
8+
- tests/CodeIndex.Tests/IndexCommandRunnerFullScanTests.cs
9+
---
10+
11+
## English
12+
13+
- **C# static readonly tuple fields retain complete metadata (#4616)** — static readonly fields now use the shared C# type grammar, so tuple, nullable-tuple, and generic-tuple-member types remain `field` symbols with complete return-type metadata without treating deconstruction locals as fields. The C# extractor contract was advanced so normal incremental indexing refreshes unchanged source files created by earlier versions.
14+
15+
## 日本語
16+
17+
- **C# の static readonly tuple field で完全なメタデータを保持するようになりました (#4616)** — static readonly field が共有 C# 型文法を使うようになり、tuple、nullable tuple、generic 内の tuple member 型を deconstruction local と誤認せず、完全な return-type metadata を持つ `field` symbol として維持します。C# extractor contract も更新したため、通常の incremental index で以前の version が作成した未変更 source file を再抽出します。

src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public static partial class SymbolExtractor
1616
public const int DefaultContractVersion = 1;
1717
public const int ExpandedLanguageContractVersion = 2;
1818
public const int PythonContractVersion = 2;
19-
public const int CSharpContractVersion = 3;
19+
public const int CSharpContractVersion = 4;
2020
public const int DockerfileContractVersion = 2;
2121
public const int MakefileContractVersion = 2;
2222
public const int StyleAndXamlContractVersion = 2;
@@ -1131,12 +1131,17 @@ private enum JavaScriptTypeScriptFunctionHeaderConsumeResult
11311131
// 先頭以外の位置に現れることを許容し、`readonly public static` や `static public readonly`
11321132
// のような旧来の並びでも kind `field` で取り扱う。通常フィールド(kind `property`)の
11331133
// 正規表現に流れ落ちないようにする。Closes #355.
1134+
// Share CSharpTypePattern with const and plain fields so tuple, nullable-tuple, and
1135+
// generic-over-tuple types retain stable field kind and complete return-type metadata.
1136+
// const / 通常フィールドと CSharpTypePattern を共有し、tuple / nullable tuple /
1137+
// generic-over-tuple 型でも安定した field kind と完全な return-type metadata を保持する。
1138+
// Closes #4616.
11341139
new("function", new Regex(
11351140
$@"^\s*"
11361141
+ $@"(?=(?:(?:{CSharpVisibilityPattern}|new|static|readonly)\s+)*static\s+)"
11371142
+ $@"(?=(?:(?:{CSharpVisibilityPattern}|new|static|readonly)\s+)*readonly\s+)"
11381143
+ $@"(?:(?<visibility>{CSharpVisibilityPattern})\s+|(?:new|static|readonly)\s+)+"
1139-
+ @"(?<returnType>[\w@?.<>\[\],:\s]+?)\s+(?<name>" + CSharpIdentifierPattern + @")\s*[=;]",
1144+
+ $@"(?<returnType>{CSharpTypePattern})\s+(?<name>{CSharpIdentifierPattern})\s*[=;]",
11401145
RegexOptions.Compiled), BodyStyle.None, "visibility", "returnType"),
11411146
// Plain field (instance, readonly, volatile, plain static, etc.) — kind `property`.
11421147
// Must come AFTER the `const` and `static readonly` patterns (which take priority

tests/CodeIndex.Tests/IndexCommandRunnerFullScanTests.cs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2204,6 +2204,68 @@ WHERE kind IN ('route', 'implements', 'attribute', 'layout')
22042204
}
22052205
}
22062206

2207+
[Fact]
2208+
public void Run_FullScan_ReindexesUnchangedCSharpTupleReadonlyFieldWhenExtractorVersionChanged_Issue4616()
2209+
{
2210+
var projectRoot = CreateTempProject();
2211+
try
2212+
{
2213+
File.WriteAllText(
2214+
Path.Combine(projectRoot, "tuple-fields.cs"),
2215+
"""
2216+
public class TupleFields
2217+
{
2218+
public static readonly (int Left, int Right) Pair = (1, 2);
2219+
}
2220+
""");
2221+
2222+
var initialExitCode = IndexCommandRunner.Run([projectRoot, "--json"], _jsonOptions);
2223+
Assert.Equal(CommandExitCodes.Success, initialExitCode);
2224+
2225+
var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db");
2226+
using (var conn = OpenNonPoolingConnection(dbPath))
2227+
{
2228+
conn.Open();
2229+
using var cmd = conn.CreateCommand();
2230+
cmd.CommandText = $"""
2231+
DELETE FROM symbols WHERE name = 'Pair';
2232+
UPDATE codeindex_meta
2233+
SET value = '3'
2234+
WHERE key = '{DbContext.GetSymbolExtractorVersionMetaKey("csharp")}';
2235+
""";
2236+
cmd.ExecuteNonQuery();
2237+
}
2238+
2239+
var (exitCode, json) = RunAndCaptureJson([projectRoot, "--json"]);
2240+
2241+
Assert.Equal(CommandExitCodes.Success, exitCode);
2242+
Assert.Equal("success", json.GetProperty("status").GetString());
2243+
Assert.Equal(0, json.GetProperty("summary").GetProperty("files_skipped").GetInt32());
2244+
2245+
using var verify = OpenNonPoolingConnection(dbPath);
2246+
verify.Open();
2247+
using var symbolCmd = verify.CreateCommand();
2248+
symbolCmd.CommandText = "SELECT kind, return_type FROM symbols WHERE name = 'Pair'";
2249+
using (var reader = symbolCmd.ExecuteReader())
2250+
{
2251+
Assert.True(reader.Read());
2252+
Assert.Equal("field", reader.GetString(0));
2253+
Assert.Equal("(int Left, int Right)", reader.GetString(1));
2254+
Assert.False(reader.Read());
2255+
}
2256+
2257+
using var versionCmd = verify.CreateCommand();
2258+
versionCmd.CommandText = $"SELECT value FROM codeindex_meta WHERE key = '{DbContext.GetSymbolExtractorVersionMetaKey("csharp")}'";
2259+
Assert.Equal(
2260+
SymbolExtractor.CSharpContractVersion.ToString(System.Globalization.CultureInfo.InvariantCulture),
2261+
versionCmd.ExecuteScalar() as string);
2262+
}
2263+
finally
2264+
{
2265+
DeleteDirectory(projectRoot);
2266+
}
2267+
}
2268+
22072269
[ProductionRuntimeFact]
22082270
public void Run_FullScan_DegradedWarningSummarizesRemainingFoldGap()
22092271
{

tests/CodeIndex.Tests/SymbolExtractorCSharpTests.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3087,6 +3087,39 @@ public class Svc
30873087
Assert.DoesNotContain(symbols, s => s.Kind == "property" && s.Name is "A" or "B" or "C" or "D" or "D2" or "E" or "F");
30883088
}
30893089

3090+
[Fact]
3091+
public void Extract_CSharp_FieldPatterns_ShareTupleTypeGrammar_Issue4616()
3092+
{
3093+
var content = """
3094+
namespace TupleFields;
3095+
3096+
public class Holder
3097+
{
3098+
public static readonly (int Left, int Right) Pair = (1, 2);
3099+
public static readonly (int Left, int Right)? MaybePair = null;
3100+
public static readonly Dictionary<string, (int Left, int Right)> PairByName = new();
3101+
public const (int Left, int Right) ConstPair = (1, 2);
3102+
public (int Left, int Right) PlainPair = (1, 2);
3103+
3104+
public void DeconstructLocals()
3105+
{
3106+
var (left, right) = Pair;
3107+
(int nestedLeft, int nestedRight) = Pair;
3108+
}
3109+
}
3110+
""";
3111+
var symbols = SymbolExtractor.Extract(1, "csharp", content);
3112+
3113+
Assert.Contains(symbols, s => s.Kind == "field" && s.Name == "Pair" && s.ReturnType == "(int Left, int Right)");
3114+
Assert.Contains(symbols, s => s.Kind == "field" && s.Name == "MaybePair" && s.ReturnType == "(int Left, int Right)?");
3115+
var pairByName = Assert.Single(symbols.Where(s => s.Name == "PairByName"));
3116+
Assert.Equal("field", pairByName.Kind);
3117+
Assert.Equal("Dictionary<string,(int Left,int Right)>", pairByName.ReturnType);
3118+
Assert.Contains(symbols, s => s.Kind == "field" && s.Name == "ConstPair" && s.ReturnType == "(int Left, int Right)");
3119+
Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "PlainPair" && s.ReturnType == "(int Left, int Right)");
3120+
Assert.DoesNotContain(symbols, s => s.Name is "left" or "right" or "nestedLeft" or "nestedRight");
3121+
}
3122+
30903123
[Fact]
30913124
public void Extract_CSharp_Method_FreeModifierOrder()
30923125
{

0 commit comments

Comments
 (0)