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
4 changes: 4 additions & 0 deletions DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1435,6 +1435,8 @@ Supported symbol kinds by language:
| Zig / PowerShell / CSS-SCSS / Batch / Assembly / HTML | language-specific functions, labels, selectors, stages, Web Components, properties, imports | language-specific references where implemented | mixed |
| XML | bounded element/attribute paths plus NuGet.config security-policy values | XAML references where implemented | mixed |

C# parameter and argument-list modifier tokens (`out`, `ref`, `in`, `params`, `this`, and `scoped`) are excluded only when parsed in modifier positions. A following concrete or generic type retains its `type_reference`, while `out var` emits neither the modifier nor the implicit `var` as a type. Do not implement this as a global keyword blacklist because contextual keywords can remain legal identifiers outside modifier positions.

Shell and PowerShell files also expose a synthetic `<script>` function symbol spanning the file. Top-level call references use this scope as their graph container, while references inside declared functions retain the declared function container.

Type aliases are indexed as `import` symbols in Rust, TypeScript, Swift, Go, F# and Scala. In F#, record declarations map to `struct`, discriminated unions map to `enum`, and constructor-style `type` declarations remain `class`.
Expand Down Expand Up @@ -4579,6 +4581,8 @@ SQL 固有の symbol extraction:
| Zig / PowerShell / CSS-SCSS / Batch / Assembly / HTML | 言語別 function、label、selector、stage、Web Component、property、import | 実装済みの言語別 reference | mixed |
| XML | 上限付き element / attribute path と NuGet.config security-policy value | 実装済みの XAML reference | mixed |

C# の parameter / argument-list modifier token(`out`、`ref`、`in`、`params`、`this`、`scoped`)は、modifier 位置として解析された場合だけ除外します。後続の concrete type / generic type の `type_reference` は維持し、`out var` では modifier も暗黙の `var` も型として出力しません。contextual keyword は modifier 位置以外では合法な identifier になり得るため、global keyword blacklist にしてはいけません。

Shell と PowerShell のファイルには、ファイル全体を覆う合成 `<script>` 関数シンボルも作成される。トップレベルの call reference はこのスコープを graph container として使い、宣言済み関数内の reference はその関数 container を維持する。

Rust / TypeScript / Swift / Go / F# / Scala の type alias は `import` シンボルとして index される。F# では record は `struct`、discriminated union は `enum`、constructor 形式の `type` は `class` として扱う。
Expand Down
2 changes: 2 additions & 0 deletions TESTING_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,7 @@ Use `docs/test-doc-maintenance-plan.md` before moving oversized suites or adding
- Nested generic attribute forms extend that shared fixture instead of running a second parser pass solely for deeper angle-bracket nesting.
- No-argument parameter attributes on methods, delegates, and lambdas share one C# fixture because all three exercise the same section-local parenthesis-depth rule.
- Argument-bearing parameter attributes share one method fixture for inline and line-broken declaration layouts.
- C# parameter and argument-list modifier coverage keeps multiline declaration and call sites for `out` / `ref` / `in` / `params` / `this` / `scoped`, multiple modifier fragments on one continuation line, final `)`-closing fragments, nested lambda arguments, `out var`, following generic types, ref returns, and ref structs in one extractor fixture. The indexed reader fixture separately proves raw `type_reference` queries exclude modifiers while resolved following-type edges remain graph-queryable.
- Direct and `global::` static type qualifiers share one C# fixture while retaining per-container reference assertions.
- Static qualifiers in using statements and field access share one consumer fixture and extraction pass.
- Namespace-qualified and Pascal-cased instance-member chains share one qualifier fixture with a rightmost static type reference, so positive and negative qualifier outcomes are checked after one parse.
Expand Down Expand Up @@ -1317,6 +1318,7 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests"
- nested generic attribute 形式も同じ共有 fixture に含め、山括弧の深い入れ子だけのために2回目の parser pass を実行しません。
- method、delegate、lambda の no-argument parameter attribute は、同じ section-local parenthesis-depth 規則を通るため1つの C# fixture を共有します。
- 引数付き parameter attribute は、inline と改行された declaration layout で1つの method fixture を共有します。
- C# の parameter / argument-list modifier coverage は、`out` / `ref` / `in` / `params` / `this` / `scoped`、1つの continuation line 上にある複数の modifier fragment、末尾が `)` で閉じる fragment、nested lambda argument、`out var`、後続の generic type、ref return、ref struct の multiline declaration / call site を1つの extractor fixture で共有します。indexed reader fixture では、modifier が raw `type_reference` query に出ず、解決済みの後続型 edge が graph query 可能なままであることを別途固定します。
- direct と `global::` の static type qualifier は、container ごとの reference assertion を維持しながら1つの C# fixture を共有します。
- using statement と field access の static qualifier は、1つの consumer fixture と抽出 pass を共有します。
- namespace qualifier と PascalCase の instance-member chain は rightmost static type reference と1つの qualifier fixture を共有し、1回の parse 後に正例と call 除外を検証します。
Expand Down
20 changes: 20 additions & 0 deletions changelog.d/unreleased/4832.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
category: fixed
issues:
- 4832
affected:
- src/CodeIndex/Indexer/References/ReferenceExtractor.DeclarationTypes.cs
- src/CodeIndex/Indexer/References/ReferenceExtractor.TypeExpressionSegments.cs
- tests/CodeIndex.Tests/ReferenceExtractorCSharpTests.cs
- tests/CodeIndex.Tests/DbReaderTests.cs
- DEVELOPER_GUIDE.md
- TESTING_GUIDE.md
---

## English

- **C# parameter modifiers no longer appear as type references (#4832)** — Modifier-position `out`, `ref`, `in`, `params`, `this`, and `scoped` tokens are excluded while following concrete and generic types retain their resolved type references, including in multiline declarations, calls, and nested lambda arguments; `out var` no longer creates a pseudo-type reference.

## 日本語

- **C# のパラメーター修飾子を型参照として出力しないようにしました (#4832)** — 修飾子位置の `out`、`ref`、`in`、`params`、`this`、`scoped` を除外しつつ、複数行の宣言・呼び出しや nested lambda argument でも後続の具体型・ジェネリック型は解決済みの型参照として維持し、`out var` は疑似的な型参照を生成しないようにしました。
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,17 @@ internal static void EmitDeclarationTypeReferences(
resolveContainerForColumn(declarationTypeStart),
ignoredSegments);
}

EmitCSharpModifierTypeReferences(
language,
line,
references,
seen,
fileId,
context,
lineNumber,
resolveContainerForColumn,
ignoredSegments);
}

internal static void EmitTypeScriptDeclarationTypeReferences(
Expand Down Expand Up @@ -669,6 +680,108 @@ private static bool TryGetParameterTypeRelativeSpan(string parameterFragment, st
return true;
}

private static void EmitCSharpModifierTypeReferences(
string language,
string line,
List<ReferenceRecord> references,
ReferenceDedupeSet seen,
long fileId,
string context,
int lineNumber,
Func<int, SymbolRecord?> resolveContainerForColumn,
IReadOnlySet<string>? ignoredSegments)
{
if (language != "csharp")
return;

for (int tokenStart = 0; tokenStart < line.Length;)
{
if (!IsTypeExpressionIdentifierStart(language, line[tokenStart]))
{
tokenStart++;
continue;
}

int tokenEnd = tokenStart + 1;
while (tokenEnd < line.Length && IsTypeExpressionIdentifierPart(language, line[tokenEnd]))
tokenEnd++;
var token = line.Substring(tokenStart, tokenEnd - tokenStart);
if (!IsCSharpParameterModifierPosition(line, tokenStart, tokenEnd, token))
{
tokenStart = tokenEnd;
continue;
}

int fragmentEnd = FindCSharpModifierFragmentEnd(line, tokenEnd);
var fragment = line.Substring(tokenStart, fragmentEnd - tokenStart);
if (TryGetParameterTypeRelativeSpan(fragment, language, out var typeRelativeStart, out var typeRelativeLength))
{
int absoluteStart = tokenStart + typeRelativeStart;
AddTypeExpressionSegmentsForLanguage(
language,
references,
seen,
fileId,
fragment.Substring(typeRelativeStart, typeRelativeLength),
absoluteStart,
context,
lineNumber,
resolveContainerForColumn(absoluteStart),
ignoredSegments);
}

tokenStart = tokenEnd;
}
}

private static int FindCSharpModifierFragmentEnd(string line, int startIndex)
{
int angleDepth = 0;
int parenDepth = 0;
int squareDepth = 0;
int braceDepth = 0;

for (int i = startIndex; i < line.Length; i++)
{
char c = line[i];
if (angleDepth == 0 && parenDepth == 0 && squareDepth == 0 && braceDepth == 0)
{
if (c is ',' or ')' or ';')
return i;
}

switch (c)
{
case '<':
angleDepth++;
break;
case '>':
if (angleDepth > 0) angleDepth--;
break;
case '(':
parenDepth++;
break;
case ')':
if (parenDepth > 0) parenDepth--;
break;
case '[':
squareDepth++;
break;
case ']':
if (squareDepth > 0) squareDepth--;
break;
case '{':
braceDepth++;
break;
case '}':
if (braceDepth > 0) braceDepth--;
break;
}
}

return line.Length;
}

private static bool TryGetSimpleDeclarationTypeSpan(string line, string language, out int typeStart, out int typeLength)
{
typeStart = -1;
Expand Down Expand Up @@ -701,7 +814,10 @@ private static bool TryGetSimpleDeclarationTypeSpan(string line, string language
while (first < tokens.Count)
{
var token = head.Substring(tokens[first].Start, tokens[first].Length);
if (token.StartsWith("[", StringComparison.Ordinal) || token.StartsWith("@", StringComparison.Ordinal) || IsDeclarationModifier(language, token))
if (token.StartsWith("[", StringComparison.Ordinal)
|| token.StartsWith("@", StringComparison.Ordinal)
|| IsDeclarationModifier(language, token)
|| IsParameterModifier(language, token))
{
first++;
continue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,14 @@ internal static void AddTypeExpressionSegments(
if (language == "csharp")
segment = NormalizeCSharpIdentifier(rawSegment);

if (language == "csharp"
&& !isEscapedCSharpIdentifier
&& IsCSharpParameterModifierPosition(expression, segmentStart, i, segment))
{
i--;
continue;
}

if (language == "kotlin" && KotlinTypeProjectionModifierNames.Contains(segment))
{
i--;
Expand Down Expand Up @@ -178,5 +186,40 @@ internal static void AddTypeExpressionSegments(
}
}

private static bool IsCSharpParameterModifierPosition(
string expression,
int segmentStart,
int segmentEnd,
string segment)
{
if (!IsParameterModifier("csharp", segment))
return false;

int next = SkipWhitespace(expression, segmentEnd);
if (next >= expression.Length
|| (expression[next] != '(' && !IsTypeExpressionIdentifierStart("csharp", expression[next])))
{
return false;
}

int previous = segmentStart - 1;
while (previous >= 0 && char.IsWhiteSpace(expression[previous]))
previous--;
if (previous < 0 || expression[previous] is '(' or ',')
return true;
if (!IsTypeExpressionIdentifierPart("csharp", expression[previous]))
return false;

int previousEnd = previous + 1;
while (previous >= 0 && IsTypeExpressionIdentifierPart("csharp", expression[previous]))
previous--;
int previousStart = previous + 1;
var previousSegment = expression.Substring(previousStart, previousEnd - previousStart);
return IsCSharpParameterModifierPosition(
expression,
previousStart,
previousEnd,
previousSegment);
}

}
129 changes: 129 additions & 0 deletions tests/CodeIndex.Tests/DbReaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,135 @@ private bool TryReadAllPositionLines(string path, out IReadOnlyList<string?> sou
Assert.Equal(1, callee.ReferenceCount);
}

[Fact]
public void CSharpParameterAndArgumentModifiersStayOutOfTypeReferenceQueries_Issue4832()
{
const string path = "src/csharp_modifier_type_references.cs";
InsertIndexedFile(
path,
"csharp",
"""
using System.Linq;

struct Payload {}
class Wrapper<T> {}

static class ModifierFixture
{
static void Params(params Wrapper<Payload>[] items) {}

static void Multi(
out Payload first, out Wrapper<Payload> second)
{
first = default;
second = default;
}

static void Final(
Payload first,
out Wrapper<Payload> last)
{
last = default;
}

static bool TryPayload(out Payload value)
{
value = default;
return true;
}

static void Consume(
out Payload output,
ref Wrapper<Payload> byRef,
in Payload input,
Payload tail)
{
output = input;
}

static void Extend(
this scoped ref Payload target,
scoped in Payload input)
{
}

static void Run(Wrapper<Payload> byRef, Payload input)
{
Consume(
out Payload declared,
ref byRef,
in input,
input);
Consume(
out var inferred,
ref byRef,
in input,
input);
Multi(
out Payload first, out Wrapper<Payload> second);
Final(
input,
out Wrapper<Payload> final);
var inferredMatch = new[] { input }.Any(item => TryPayload(out var nested) && nested.Equals(item));
var explicitMatch = new[] { input }.Any(item => TryPayload(out Payload nested) && nested.Equals(item));
}
}
""");

foreach (var modifier in new[] { "out", "ref", "in", "params", "this", "scoped" })
{
Assert.Empty(_reader.SearchReferences(
modifier,
lang: "csharp",
referenceKind: "type_reference",
exact: true,
pathPatterns: [path]));
}

var payloadReferences = _reader.SearchReferences(
"Payload",
lang: "csharp",
referenceKind: "type_reference",
exact: true,
pathPatterns: [path]);
Assert.Contains(payloadReferences, reference =>
reference.Context.Contains("out Payload output", StringComparison.Ordinal));
Assert.Contains(payloadReferences, reference =>
reference.Context.Contains("this scoped ref Payload target", StringComparison.Ordinal));
Assert.Contains(payloadReferences, reference =>
reference.Context.Contains("TryPayload(out Payload nested)", StringComparison.Ordinal));
Assert.All(payloadReferences, reference => Assert.Equal("resolved", reference.ResolutionState));

var wrapperReferences = _reader.SearchReferences(
"Wrapper",
lang: "csharp",
referenceKind: "type_reference",
exact: true,
pathPatterns: [path]);
Assert.Contains(wrapperReferences, reference =>
reference.Context.Contains("ref Wrapper<Payload> byRef", StringComparison.Ordinal));
Assert.Equal(2, wrapperReferences.Count(reference =>
reference.Context.Contains("out Payload first, out Wrapper<Payload> second", StringComparison.Ordinal)));
Assert.Contains(wrapperReferences, reference =>
reference.Context.Contains("out Wrapper<Payload> last)", StringComparison.Ordinal));
Assert.Contains(wrapperReferences, reference =>
reference.Context.Contains("out Wrapper<Payload> final)", StringComparison.Ordinal));
Assert.All(wrapperReferences, reference => Assert.Equal("resolved", reference.ResolutionState));

var callers = _reader.GetCallers(
"Payload",
lang: "csharp",
referenceKind: "type_reference",
exact: true,
pathPatterns: [path]);
Assert.NotEmpty(callers);
Assert.All(callers, caller =>
{
Assert.Equal("Payload", caller.CalleeName);
Assert.Equal("type_reference", caller.ReferenceKind);
});
}

[Fact]
public void GetCallers_SolutionProjectReference_RequiresExplicitKind_Issue3662()
{
Expand Down
Loading
Loading