Skip to content

Commit 4bdfecc

Browse files
authored
Fix C# parameter modifier type references (#4924)
* Fix C# parameter modifier type references (#4832) * Handle C# modifier continuation fragments (#4832) * Handle nested C# parameter modifiers (#4832)
1 parent 797667e commit 4bdfecc

7 files changed

Lines changed: 447 additions & 1 deletion

File tree

DEVELOPER_GUIDE.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1435,6 +1435,8 @@ Supported symbol kinds by language:
14351435
| Zig / PowerShell / CSS-SCSS / Batch / Assembly / HTML | language-specific functions, labels, selectors, stages, Web Components, properties, imports | language-specific references where implemented | mixed |
14361436
| XML | bounded element/attribute paths plus NuGet.config security-policy values | XAML references where implemented | mixed |
14371437

1438+
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.
1439+
14381440
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.
14391441

14401442
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`.
@@ -4580,6 +4582,8 @@ SQL 固有の symbol extraction:
45804582
| Zig / PowerShell / CSS-SCSS / Batch / Assembly / HTML | 言語別 function、label、selector、stage、Web Component、property、import | 実装済みの言語別 reference | mixed |
45814583
| XML | 上限付き element / attribute path と NuGet.config security-policy value | 実装済みの XAML reference | mixed |
45824584

4585+
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 にしてはいけません。
4586+
45834587
Shell と PowerShell のファイルには、ファイル全体を覆う合成 `<script>` 関数シンボルも作成される。トップレベルの call reference はこのスコープを graph container として使い、宣言済み関数内の reference はその関数 container を維持する。
45844588

45854589
Rust / TypeScript / Swift / Go / F# / Scala の type alias は `import` シンボルとして index される。F# では record は `struct`、discriminated union は `enum`、constructor 形式の `type` は `class` として扱う。

TESTING_GUIDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,7 @@ Use `docs/test-doc-maintenance-plan.md` before moving oversized suites or adding
434434
- Nested generic attribute forms extend that shared fixture instead of running a second parser pass solely for deeper angle-bracket nesting.
435435
- No-argument parameter attributes on methods, delegates, and lambdas share one C# fixture because all three exercise the same section-local parenthesis-depth rule.
436436
- Argument-bearing parameter attributes share one method fixture for inline and line-broken declaration layouts.
437+
- 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.
437438
- Direct and `global::` static type qualifiers share one C# fixture while retaining per-container reference assertions.
438439
- Static qualifiers in using statements and field access share one consumer fixture and extraction pass.
439440
- 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.
@@ -1319,6 +1320,7 @@ dotnet test --filter "FullyQualifiedName~GitHelperTests"
13191320
- nested generic attribute 形式も同じ共有 fixture に含め、山括弧の深い入れ子だけのために2回目の parser pass を実行しません。
13201321
- method、delegate、lambda の no-argument parameter attribute は、同じ section-local parenthesis-depth 規則を通るため1つの C# fixture を共有します。
13211322
- 引数付き parameter attribute は、inline と改行された declaration layout で1つの method fixture を共有します。
1323+
- 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 可能なままであることを別途固定します。
13221324
- direct と `global::` の static type qualifier は、container ごとの reference assertion を維持しながら1つの C# fixture を共有します。
13231325
- using statement と field access の static qualifier は、1つの consumer fixture と抽出 pass を共有します。
13241326
- namespace qualifier と PascalCase の instance-member chain は rightmost static type reference と1つの qualifier fixture を共有し、1回の parse 後に正例と call 除外を検証します。
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
category: fixed
3+
issues:
4+
- 4832
5+
affected:
6+
- src/CodeIndex/Indexer/References/ReferenceExtractor.DeclarationTypes.cs
7+
- src/CodeIndex/Indexer/References/ReferenceExtractor.TypeExpressionSegments.cs
8+
- tests/CodeIndex.Tests/ReferenceExtractorCSharpTests.cs
9+
- tests/CodeIndex.Tests/DbReaderTests.cs
10+
- DEVELOPER_GUIDE.md
11+
- TESTING_GUIDE.md
12+
---
13+
14+
## English
15+
16+
- **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.
17+
18+
## 日本語
19+
20+
- **C# のパラメーター修飾子を型参照として出力しないようにしました (#4832)** — 修飾子位置の `out``ref``in``params``this``scoped` を除外しつつ、複数行の宣言・呼び出しや nested lambda argument でも後続の具体型・ジェネリック型は解決済みの型参照として維持し、`out var` は疑似的な型参照を生成しないようにしました。

src/CodeIndex/Indexer/References/ReferenceExtractor.DeclarationTypes.cs

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,17 @@ internal static void EmitDeclarationTypeReferences(
6363
resolveContainerForColumn(declarationTypeStart),
6464
ignoredSegments);
6565
}
66+
67+
EmitCSharpModifierTypeReferences(
68+
language,
69+
line,
70+
references,
71+
seen,
72+
fileId,
73+
context,
74+
lineNumber,
75+
resolveContainerForColumn,
76+
ignoredSegments);
6677
}
6778

6879
internal static void EmitTypeScriptDeclarationTypeReferences(
@@ -669,6 +680,108 @@ private static bool TryGetParameterTypeRelativeSpan(string parameterFragment, st
669680
return true;
670681
}
671682

683+
private static void EmitCSharpModifierTypeReferences(
684+
string language,
685+
string line,
686+
List<ReferenceRecord> references,
687+
ReferenceDedupeSet seen,
688+
long fileId,
689+
string context,
690+
int lineNumber,
691+
Func<int, SymbolRecord?> resolveContainerForColumn,
692+
IReadOnlySet<string>? ignoredSegments)
693+
{
694+
if (language != "csharp")
695+
return;
696+
697+
for (int tokenStart = 0; tokenStart < line.Length;)
698+
{
699+
if (!IsTypeExpressionIdentifierStart(language, line[tokenStart]))
700+
{
701+
tokenStart++;
702+
continue;
703+
}
704+
705+
int tokenEnd = tokenStart + 1;
706+
while (tokenEnd < line.Length && IsTypeExpressionIdentifierPart(language, line[tokenEnd]))
707+
tokenEnd++;
708+
var token = line.Substring(tokenStart, tokenEnd - tokenStart);
709+
if (!IsCSharpParameterModifierPosition(line, tokenStart, tokenEnd, token))
710+
{
711+
tokenStart = tokenEnd;
712+
continue;
713+
}
714+
715+
int fragmentEnd = FindCSharpModifierFragmentEnd(line, tokenEnd);
716+
var fragment = line.Substring(tokenStart, fragmentEnd - tokenStart);
717+
if (TryGetParameterTypeRelativeSpan(fragment, language, out var typeRelativeStart, out var typeRelativeLength))
718+
{
719+
int absoluteStart = tokenStart + typeRelativeStart;
720+
AddTypeExpressionSegmentsForLanguage(
721+
language,
722+
references,
723+
seen,
724+
fileId,
725+
fragment.Substring(typeRelativeStart, typeRelativeLength),
726+
absoluteStart,
727+
context,
728+
lineNumber,
729+
resolveContainerForColumn(absoluteStart),
730+
ignoredSegments);
731+
}
732+
733+
tokenStart = tokenEnd;
734+
}
735+
}
736+
737+
private static int FindCSharpModifierFragmentEnd(string line, int startIndex)
738+
{
739+
int angleDepth = 0;
740+
int parenDepth = 0;
741+
int squareDepth = 0;
742+
int braceDepth = 0;
743+
744+
for (int i = startIndex; i < line.Length; i++)
745+
{
746+
char c = line[i];
747+
if (angleDepth == 0 && parenDepth == 0 && squareDepth == 0 && braceDepth == 0)
748+
{
749+
if (c is ',' or ')' or ';')
750+
return i;
751+
}
752+
753+
switch (c)
754+
{
755+
case '<':
756+
angleDepth++;
757+
break;
758+
case '>':
759+
if (angleDepth > 0) angleDepth--;
760+
break;
761+
case '(':
762+
parenDepth++;
763+
break;
764+
case ')':
765+
if (parenDepth > 0) parenDepth--;
766+
break;
767+
case '[':
768+
squareDepth++;
769+
break;
770+
case ']':
771+
if (squareDepth > 0) squareDepth--;
772+
break;
773+
case '{':
774+
braceDepth++;
775+
break;
776+
case '}':
777+
if (braceDepth > 0) braceDepth--;
778+
break;
779+
}
780+
}
781+
782+
return line.Length;
783+
}
784+
672785
private static bool TryGetSimpleDeclarationTypeSpan(string line, string language, out int typeStart, out int typeLength)
673786
{
674787
typeStart = -1;
@@ -701,7 +814,10 @@ private static bool TryGetSimpleDeclarationTypeSpan(string line, string language
701814
while (first < tokens.Count)
702815
{
703816
var token = head.Substring(tokens[first].Start, tokens[first].Length);
704-
if (token.StartsWith("[", StringComparison.Ordinal) || token.StartsWith("@", StringComparison.Ordinal) || IsDeclarationModifier(language, token))
817+
if (token.StartsWith("[", StringComparison.Ordinal)
818+
|| token.StartsWith("@", StringComparison.Ordinal)
819+
|| IsDeclarationModifier(language, token)
820+
|| IsParameterModifier(language, token))
705821
{
706822
first++;
707823
continue;

src/CodeIndex/Indexer/References/ReferenceExtractor.TypeExpressionSegments.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,14 @@ internal static void AddTypeExpressionSegments(
149149
if (language == "csharp")
150150
segment = NormalizeCSharpIdentifier(rawSegment);
151151

152+
if (language == "csharp"
153+
&& !isEscapedCSharpIdentifier
154+
&& IsCSharpParameterModifierPosition(expression, segmentStart, i, segment))
155+
{
156+
i--;
157+
continue;
158+
}
159+
152160
if (language == "kotlin" && KotlinTypeProjectionModifierNames.Contains(segment))
153161
{
154162
i--;
@@ -178,5 +186,40 @@ internal static void AddTypeExpressionSegments(
178186
}
179187
}
180188

189+
private static bool IsCSharpParameterModifierPosition(
190+
string expression,
191+
int segmentStart,
192+
int segmentEnd,
193+
string segment)
194+
{
195+
if (!IsParameterModifier("csharp", segment))
196+
return false;
197+
198+
int next = SkipWhitespace(expression, segmentEnd);
199+
if (next >= expression.Length
200+
|| (expression[next] != '(' && !IsTypeExpressionIdentifierStart("csharp", expression[next])))
201+
{
202+
return false;
203+
}
204+
205+
int previous = segmentStart - 1;
206+
while (previous >= 0 && char.IsWhiteSpace(expression[previous]))
207+
previous--;
208+
if (previous < 0 || expression[previous] is '(' or ',')
209+
return true;
210+
if (!IsTypeExpressionIdentifierPart("csharp", expression[previous]))
211+
return false;
212+
213+
int previousEnd = previous + 1;
214+
while (previous >= 0 && IsTypeExpressionIdentifierPart("csharp", expression[previous]))
215+
previous--;
216+
int previousStart = previous + 1;
217+
var previousSegment = expression.Substring(previousStart, previousEnd - previousStart);
218+
return IsCSharpParameterModifierPosition(
219+
expression,
220+
previousStart,
221+
previousEnd,
222+
previousSegment);
223+
}
181224

182225
}

tests/CodeIndex.Tests/DbReaderTests.cs

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,135 @@ private bool TryReadAllPositionLines(string path, out IReadOnlyList<string?> sou
401401
Assert.Equal(1, callee.ReferenceCount);
402402
}
403403

404+
[Fact]
405+
public void CSharpParameterAndArgumentModifiersStayOutOfTypeReferenceQueries_Issue4832()
406+
{
407+
const string path = "src/csharp_modifier_type_references.cs";
408+
InsertIndexedFile(
409+
path,
410+
"csharp",
411+
"""
412+
using System.Linq;
413+
414+
struct Payload {}
415+
class Wrapper<T> {}
416+
417+
static class ModifierFixture
418+
{
419+
static void Params(params Wrapper<Payload>[] items) {}
420+
421+
static void Multi(
422+
out Payload first, out Wrapper<Payload> second)
423+
{
424+
first = default;
425+
second = default;
426+
}
427+
428+
static void Final(
429+
Payload first,
430+
out Wrapper<Payload> last)
431+
{
432+
last = default;
433+
}
434+
435+
static bool TryPayload(out Payload value)
436+
{
437+
value = default;
438+
return true;
439+
}
440+
441+
static void Consume(
442+
out Payload output,
443+
ref Wrapper<Payload> byRef,
444+
in Payload input,
445+
Payload tail)
446+
{
447+
output = input;
448+
}
449+
450+
static void Extend(
451+
this scoped ref Payload target,
452+
scoped in Payload input)
453+
{
454+
}
455+
456+
static void Run(Wrapper<Payload> byRef, Payload input)
457+
{
458+
Consume(
459+
out Payload declared,
460+
ref byRef,
461+
in input,
462+
input);
463+
Consume(
464+
out var inferred,
465+
ref byRef,
466+
in input,
467+
input);
468+
Multi(
469+
out Payload first, out Wrapper<Payload> second);
470+
Final(
471+
input,
472+
out Wrapper<Payload> final);
473+
var inferredMatch = new[] { input }.Any(item => TryPayload(out var nested) && nested.Equals(item));
474+
var explicitMatch = new[] { input }.Any(item => TryPayload(out Payload nested) && nested.Equals(item));
475+
}
476+
}
477+
""");
478+
479+
foreach (var modifier in new[] { "out", "ref", "in", "params", "this", "scoped" })
480+
{
481+
Assert.Empty(_reader.SearchReferences(
482+
modifier,
483+
lang: "csharp",
484+
referenceKind: "type_reference",
485+
exact: true,
486+
pathPatterns: [path]));
487+
}
488+
489+
var payloadReferences = _reader.SearchReferences(
490+
"Payload",
491+
lang: "csharp",
492+
referenceKind: "type_reference",
493+
exact: true,
494+
pathPatterns: [path]);
495+
Assert.Contains(payloadReferences, reference =>
496+
reference.Context.Contains("out Payload output", StringComparison.Ordinal));
497+
Assert.Contains(payloadReferences, reference =>
498+
reference.Context.Contains("this scoped ref Payload target", StringComparison.Ordinal));
499+
Assert.Contains(payloadReferences, reference =>
500+
reference.Context.Contains("TryPayload(out Payload nested)", StringComparison.Ordinal));
501+
Assert.All(payloadReferences, reference => Assert.Equal("resolved", reference.ResolutionState));
502+
503+
var wrapperReferences = _reader.SearchReferences(
504+
"Wrapper",
505+
lang: "csharp",
506+
referenceKind: "type_reference",
507+
exact: true,
508+
pathPatterns: [path]);
509+
Assert.Contains(wrapperReferences, reference =>
510+
reference.Context.Contains("ref Wrapper<Payload> byRef", StringComparison.Ordinal));
511+
Assert.Equal(2, wrapperReferences.Count(reference =>
512+
reference.Context.Contains("out Payload first, out Wrapper<Payload> second", StringComparison.Ordinal)));
513+
Assert.Contains(wrapperReferences, reference =>
514+
reference.Context.Contains("out Wrapper<Payload> last)", StringComparison.Ordinal));
515+
Assert.Contains(wrapperReferences, reference =>
516+
reference.Context.Contains("out Wrapper<Payload> final)", StringComparison.Ordinal));
517+
Assert.All(wrapperReferences, reference => Assert.Equal("resolved", reference.ResolutionState));
518+
519+
var callers = _reader.GetCallers(
520+
"Payload",
521+
lang: "csharp",
522+
referenceKind: "type_reference",
523+
exact: true,
524+
pathPatterns: [path]);
525+
Assert.NotEmpty(callers);
526+
Assert.All(callers, caller =>
527+
{
528+
Assert.Equal("Payload", caller.CalleeName);
529+
Assert.Equal("type_reference", caller.ReferenceKind);
530+
});
531+
}
532+
404533
[Fact]
405534
public void GetCallers_SolutionProjectReference_RequiresExplicitKind_Issue3662()
406535
{

0 commit comments

Comments
 (0)