diff --git a/changelog.d/unreleased/2710.fixed.md b/changelog.d/unreleased/2710.fixed.md new file mode 100644 index 0000000000..d5e407a091 --- /dev/null +++ b/changelog.d/unreleased/2710.fixed.md @@ -0,0 +1,18 @@ +--- +category: fixed +issues: + - 2710 + - 2711 + - 2717 +affected: + - src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs + - tests/CodeIndex.Tests/SymbolExtractorTests.cs +--- + +## English + +- **Full self-indexing no longer stalls while extracting C# symbols (#2710, #2711, #2717)** — C# symbol extraction now reuses the per-line multi-line member candidate during pattern matching, avoiding repeated expensive lookahead on large source files. + +## 日本語 + +- **C# シンボル抽出中に full self-indexing が停止しないようになりました (#2710, #2711, #2717)** — C# シンボル抽出はパターン照合中に行単位の複数行メンバー候補を再利用し、大きなソースファイルで高コストな先読みを繰り返さないようになりました。 diff --git a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs index 332b496d9e..6d24557f17 100644 --- a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs +++ b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs @@ -2447,6 +2447,7 @@ public static List Extract(long fileId, string? lang, string conte { var stopAfterFirstPatternMatch = false; var restartPatternScanOffset = -1; + CSharpPropertyMatchCandidate? csharpPropertyCandidateForLine = null; foreach (var pattern in patterns) { if (lang == "csharp" && ReferenceEquals(pattern.Regex, CSharpEnumMemberRegex)) @@ -2463,7 +2464,7 @@ public static List Extract(long fileId, string? lang, string conte // function パターンは `CSharpPropertyHeaderPrefixRegex` が `(` や `{` を含む行を // 受け付けないため影響を受けず、merger は元の行をそのまま返す。Closes #355. var csharpPropertyCandidate = lang == "csharp" && pattern.Kind is "property" or "function" - ? BuildCSharpPropertyMatchLine(lines, csharpMatchLines!, i) + ? csharpPropertyCandidateForLine ??= BuildCSharpPropertyMatchLine(lines, csharpMatchLines!, i) : new CSharpPropertyMatchCandidate(matchLine, i, i); var patternMatchLine = csharpPropertyCandidate.MatchLine; if (fortranContinuationCandidate != null) diff --git a/tests/CodeIndex.Tests/SymbolExtractorTests.cs b/tests/CodeIndex.Tests/SymbolExtractorTests.cs index b4107e5a3f..3520f816ba 100644 --- a/tests/CodeIndex.Tests/SymbolExtractorTests.cs +++ b/tests/CodeIndex.Tests/SymbolExtractorTests.cs @@ -24399,6 +24399,28 @@ public void Extract_CSharp_InstallScriptFixture_CompletesWithinPracticalBudget() $"InstallScriptTests.cs extraction took {stopwatch.Elapsed.TotalSeconds:F2}s, expected < {runawayBudget.TotalSeconds:F0}s runaway guard budget."); } + [Fact] + public void Extract_CSharp_ReferenceExtractorFixture_CompletesWithinPracticalBudget() + { + // issue #2710/#2711/#2717 regression: full self-indexing could spend minutes + // repeatedly rebuilding the same multi-line C# member candidate while scanning large + // extractor sources. Keep this as a broad runaway guard for the realistic file that + // reproduced the stall on origin/main. + var path = Path.Combine(GetRepositoryRoot(), "src", "CodeIndex", "Indexer", "References", "ReferenceExtractor.cs"); + var content = File.ReadAllText(path); + + var stopwatch = Stopwatch.StartNew(); + var symbols = SymbolExtractor.Extract(1, "csharp", content); + stopwatch.Stop(); + + Assert.Contains(symbols, s => s.Kind == "class" && s.Name == "ReferenceExtractor"); + Assert.Contains(symbols, s => s.Kind == "function" && s.Name == "Extract"); + var runawayBudget = TimeSpan.FromSeconds(30); + Assert.True( + stopwatch.Elapsed < runawayBudget, + $"ReferenceExtractor.cs extraction took {stopwatch.Elapsed.TotalSeconds:F2}s, expected < {runawayBudget.TotalSeconds:F0}s runaway guard budget."); + } + [Fact] public void Extract_Java_SameLineAnnotationsCompactConstructorsAndEnumOverrides_StayIndexed() {