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
8 changes: 8 additions & 0 deletions DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,10 @@ Container ownership follows the same contract. If references repeatedly resolve
an extracted declaration by name and source range, index candidates by name once
and scan only that name's ordered range list. Preserve first-candidate behavior
for duplicate names and keep the index local to one extraction call.
For C#, both symbol assignment and reference resolution prefer the narrowest
active callable range, including `test.method` and nested local functions, before
an enclosing type. Named lambdas without a complete body range attach to that
enclosing callable and do not become reference containers.
Symbol container assignment keeps one reusable path buffer for the sorted
per-symbol walk. Enumerate the active stack into that buffer and reverse it to
outer-to-inner order; do not materialize both a stack array and a fresh path list
Expand Down Expand Up @@ -3648,6 +3652,10 @@ container ownership にも同じ契約を適用する。reference が extracted
source range で繰り返し解決する場合は、candidate を name ごとに一度だけ索引化し、その name の
ordered range list だけを走査する。duplicate name の first-candidate behavior を維持し、index は
1 回の extraction call 内だけに保持する。
C# では symbol assignment と reference resolution の両方が、enclosing type より先に、
`test.method` や nested local function を含む最も狭い active callable range を選ぶ。
完全な body range を持たない named lambda はその enclosing callable に所属し、
reference container にはしない。
symbol の container assignment は、sort 済みの per-symbol walk で1つの path buffer を再利用する。
active stack を buffer へ列挙して outer-to-inner 順に反転し、深く nest した生成ファイルの member
ごとに stack array と新しい path list の両方を実体化してはならない。
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ membership checks reuse file-local lookup sets instead of rescanning every
extracted symbol for each call site.
C# declaration-container resolution and GitHub Actions job ownership likewise
use name-indexed candidates instead of a full container scan per reference.
For C#, symbol and reference ownership prefers the narrowest active callable,
including test methods and nested local functions, before the enclosing type.
Dense Python import, GitHub Actions dependency, JSON path, and Fortran procedure
lists are scanned in place instead of allocating temporary split arrays.
Python PEP 695 aliases and `TypeAlias` / `NewType` declarations are persisted as
Expand Down Expand Up @@ -550,6 +552,8 @@ reference extraction でも、C# property と Python import / class の反復 me
call site ごとに全 extracted symbol を再走査せず、file-local な lookup set を再利用します。
C# declaration-container 解決と GitHub Actions の job ownership も同様に、reference ごとの
全 container 走査ではなく name-indexed candidate を使います。
C# の symbol / reference ownership は、enclosing type より先に、test method や nested local
function を含む最も狭い active callable を選びます。
密な Python import、GitHub Actions dependency、JSON path、Fortran procedure list は、
一時的な split array を作らず入力上で直接走査します。
Python の PEP 695 alias と `TypeAlias` / `NewType` declaration は `typealias`、
Expand Down
2 changes: 2 additions & 0 deletions TESTING_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ Use `docs/test-doc-maintenance-plan.md` before moving oversized suites or adding
- `SymbolExtractor*Tests.cs` and `ReferenceExtractor*Tests.cs`
Extractor coverage is split by language or feature area with partial test classes, while shared helpers remain on the root `SymbolExtractorTests` / `ReferenceExtractorTests` parts.
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# 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.
When moving repeated extractor scenarios out of a giant suite, keep the new partial file grouped by a readable domain such as language, build-file format, or protocol surface, and prefer small semantic assertion helpers over repeated raw substring or predicate assertions.
Expand Down Expand Up @@ -975,6 +976,7 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests"
- `SymbolExtractor*Tests.cs` と `ReferenceExtractor*Tests.cs`
extractor のカバレッジは言語または機能領域ごとの partial test class に分割し、共有 helper は root 側の `SymbolExtractorTests` / `ReferenceExtractorTests` に残します。
C# の declaration boundary に関する regression では、extractor を直接呼ぶ fixture と、実 index に対する `symbols --exact-name` query を組み合わせてください。呼び出し・parameter の continuation と、正当な複数行 method、constructor、delegate、local function を同居させ、false positive の拒否と宣言 range の両方を観測可能にします。
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 に使わないでください。
巨大 suite から繰り返しの extractor シナリオを切り出す場合は、言語、build-file 形式、protocol surface など読みやすい領域ごとの partial file にまとめ、raw substring や predicate assertion の繰り返しより小さな semantic assertion helper を優先してください。
Expand Down
22 changes: 22 additions & 0 deletions changelog.d/unreleased/4840.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
category: fixed
issues:
- 4840
affected:
- src/CodeIndex/Indexer/CallableContainerSelection.cs
- src/CodeIndex/Indexer/References
- src/CodeIndex/Indexer/Symbols/SymbolExtractor.ContainerAssignment.cs
- tests/CodeIndex.Tests/ReferenceExtractorCSharpTests.cs
- tests/CodeIndex.Tests/SymbolExtractorCSharpTests.cs
- README.md
- DEVELOPER_GUIDE.md
- TESTING_GUIDE.md
---

## English

- Preserve the innermost C# callable as the symbol and reference container for test methods, local functions, nested local functions, and named lambdas, while retaining the enclosing callable or type fallback for incomplete inner ranges. This restores the containment behavior expected by #233 and #4417.

## 日本語

- C# の test method、local function、nested local function、named lambda について、symbol と reference の container に最内側の callable を保持し、不完全な内側 range では enclosing callable または type への fallback を維持しました。これにより #233 と #4417 で想定された containment を復元しました。
42 changes: 42 additions & 0 deletions src/CodeIndex/Indexer/CallableContainerSelection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using CodeIndex.Models;

namespace CodeIndex.Indexer;

internal static class CallableContainerSelection
{
internal static bool IsCallableKind(string? kind)
=> kind is "function"
or "test.method"
or "operator"
or "lambda"
or "async_function"
or "generator"
or "async_generator";

internal static int CompareInnermost(
SymbolRecord left,
int leftOriginalIndex,
SymbolRecord right,
int rightOriginalIndex,
bool preferCallable)
{
var compare = GetSpanLength(left).CompareTo(GetSpanLength(right));
if (compare != 0)
return compare;

if (preferCallable)
{
compare = GetCallableRank(left.Kind).CompareTo(GetCallableRank(right.Kind));
if (compare != 0)
return compare;
}

return leftOriginalIndex.CompareTo(rightOriginalIndex);
}

internal static int GetSpanLength(SymbolRecord symbol)
=> (symbol.BodyEndLine ?? symbol.EndLine) - (symbol.BodyStartLine ?? symbol.StartLine);

private static int GetCallableRank(string? kind)
=> IsCallableKind(kind) ? 0 : 1;
}
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,10 @@ private static List<ReferenceRecord> ExtractFunctionalLanguageReferences(Referen
var seen = CreateReferenceSeenSet(preparedInput.Lines.Length);
var symbolsByLine = BuildFunctionalSymbolsByLine(request.Symbols, request.ReportDiagnostic);
var containerResolver = new InnermostContainerResolver(
BuildReferenceContainerCandidates(request.Symbols, request.ReportDiagnostic));
BuildReferenceContainerCandidates(
request.Language,
request.Symbols,
request.ReportDiagnostic));
var state = new FunctionalReferenceState();

for (var index = 0; index < preparedInput.Lines.Length; index++)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ internal static bool IsSafetyCapDiagnosticKind(string kind)
};

private static bool IsFunctionLikeSymbolKind(string kind)
=> kind is "function" or "operator" or "lambda" or "async_function" or "generator" or "async_generator";
=> CallableContainerSelection.IsCallableKind(kind);

private static readonly Dictionary<string, HashSet<string>> LanguageSpecificIgnoredCallNames = new(StringComparer.Ordinal)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,24 +56,28 @@ internal static void AddTypeReferenceSegment(
internal sealed class InnermostContainerResolver
{
private readonly IReadOnlyList<SymbolRecord> candidates;
private readonly List<(SymbolRecord Symbol, int SpanLength, int OriginalIndex)>? candidatesByStart;
private readonly List<(SymbolRecord Symbol, int OriginalIndex)>? candidatesByStart;
private SortedSet<ActiveContainer>? activeContainers;
private int nextCandidateIndex;
private int currentLine;
private int? cachedLine;
private SymbolRecord? cachedContainer;
private readonly bool preferCallable;

internal InnermostContainerResolver(IReadOnlyList<SymbolRecord> candidates)
internal InnermostContainerResolver(
IReadOnlyList<SymbolRecord> candidates,
bool preferCallable = false)
{
this.candidates = candidates;
this.preferCallable = preferCallable;
if (candidates.Count == 0)
return;

candidatesByStart = new List<(SymbolRecord Symbol, int SpanLength, int OriginalIndex)>(candidates.Count);
candidatesByStart = new List<(SymbolRecord Symbol, int OriginalIndex)>(candidates.Count);
for (var index = 0; index < candidates.Count; index++)
{
var symbol = candidates[index];
candidatesByStart.Add((symbol, GetContainerSpanLength(symbol), index));
candidatesByStart.Add((symbol, index));
}

candidatesByStart.Sort(CompareCandidatesByStart);
Expand Down Expand Up @@ -106,7 +110,10 @@ private void AdvanceTo(int lineNumber)
&& candidatesByStart[nextCandidateIndex].Symbol.BodyStartLine!.Value <= lineNumber)
{
var candidate = candidatesByStart[nextCandidateIndex];
(activeContainers ??= []).Add(new ActiveContainer(candidate.Symbol, candidate.SpanLength, candidate.OriginalIndex));
(activeContainers ??= []).Add(new ActiveContainer(
candidate.Symbol,
candidate.OriginalIndex,
preferCallable));
nextCandidateIndex++;
}

Expand All @@ -121,12 +128,9 @@ private void AdvanceTo(int lineNumber)
return container;
}

private static int GetContainerSpanLength(SymbolRecord symbol) =>
(symbol.BodyEndLine ?? symbol.EndLine) - (symbol.BodyStartLine ?? symbol.StartLine);

private static int CompareCandidatesByStart(
(SymbolRecord Symbol, int SpanLength, int OriginalIndex) left,
(SymbolRecord Symbol, int SpanLength, int OriginalIndex) right)
(SymbolRecord Symbol, int OriginalIndex) left,
(SymbolRecord Symbol, int OriginalIndex) right)
{
var compare = left.Symbol.BodyStartLine!.Value.CompareTo(right.Symbol.BodyStartLine!.Value);
if (compare != 0)
Expand All @@ -136,23 +140,21 @@ private static int CompareCandidatesByStart(
if (compare != 0)
return compare;

compare = left.SpanLength.CompareTo(right.SpanLength);
if (compare != 0)
return compare;

return left.OriginalIndex.CompareTo(right.OriginalIndex);
}

private readonly record struct ActiveContainer(SymbolRecord Symbol, int SpanLength, int OriginalIndex) : IComparable<ActiveContainer>
private readonly record struct ActiveContainer(
SymbolRecord Symbol,
int OriginalIndex,
bool PreferCallable) : IComparable<ActiveContainer>
{
public int CompareTo(ActiveContainer other)
{
var spanComparison = SpanLength.CompareTo(other.SpanLength);
if (spanComparison != 0)
return spanComparison;

return OriginalIndex.CompareTo(other.OriginalIndex);
}
=> CallableContainerSelection.CompareInnermost(
Symbol,
OriginalIndex,
other.Symbol,
other.OriginalIndex,
PreferCallable);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,13 @@ internal static List<ReferenceRecord> ExtractCore(
? BuildCobolCallableSymbols(symbols)
: null;
var containerCandidates = BuildReferenceContainerCandidates(
language,
symbols,
request.ReportDiagnostic);
var containerResolver =
new InnermostContainerResolver(containerCandidates);
new InnermostContainerResolver(
containerCandidates,
preferCallable: language == "csharp");
if (language == "solidity")
{
return ExtractSolidityReferences(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ private sealed class CoreExtractionLookups
private HashSet<(string Container, string Name)>? csharpProperties;
private bool csharpPropertiesResolved;
private Dictionary<string, List<SymbolRecord>>? csharpContainerCandidatesByName;
private List<(int StartLine, int StartColumn, int EndLine, int EndColumn, SymbolRecord Container)>? recordPrimaryCtorRanges;
private List<(int StartLine, int StartColumn, int EndLine, int EndColumn, SymbolRecord Container, SymbolRecord Owner)>? recordPrimaryCtorRanges;
private bool recordPrimaryCtorRangesResolved;
private (
IReadOnlyDictionary<string, CSharpContainingTypeValueReceiverNames> ByContainingType,
Expand Down Expand Up @@ -254,7 +254,7 @@ internal IReadOnlyList<SymbolRecord> GetEnclosingTypeCandidates()
return powershellSplatAssignments!;
}

internal List<(int StartLine, int StartColumn, int EndLine, int EndColumn, SymbolRecord Container)> GetRecordPrimaryCtorRanges()
internal List<(int StartLine, int StartColumn, int EndLine, int EndColumn, SymbolRecord Container, SymbolRecord Owner)> GetRecordPrimaryCtorRanges()
{
if (!recordPrimaryCtorRangesResolved)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,25 +240,44 @@ private static CSharpMultiLineTypePatternState EmitCoreReferenceLines(
{
if (language == "csharp")
{
SymbolRecord? primaryCtorOwner = null;
foreach (var (
rangeStart,
rangeStartColumn,
rangeEnd,
rangeEndColumn,
syntheticRecordCtor) in
syntheticRecordCtor,
owner) in
lookups.GetRecordPrimaryCtorRanges())
{
if (ReferenceEquals(container, syntheticRecordCtor)
|| (container?.Kind == "function"
&& container.FileId == syntheticRecordCtor.FileId
&& container.StartLine == syntheticRecordCtor.StartLine
&& (container.StartLine < rangeEnd
|| (container.StartColumn is int containerStartColumn
&& containerStartColumn < rangeEndColumn))
&& string.Equals(
container.Name,
syntheticRecordCtor.Name,
StringComparison.Ordinal)))
{
primaryCtorOwner ??= owner;
}
if (lineNumber < rangeStart || lineNumber > rangeEnd)
continue;
if (lineNumber == rangeStart
&& column < rangeStartColumn)
{
continue;
return owner;
}
if (lineNumber == rangeEnd && column >= rangeEndColumn)
continue;
return syntheticRecordCtor;
}

if (primaryCtorOwner != null)
return primaryCtorOwner;
}

if (javaSameLineCtor != null)
Expand Down
Loading
Loading