Skip to content

Commit a926ae7

Browse files
Show a hover for the indexer of a dynamic index access
A dynamic index access (a[b]) gave its IndexerExpression a DynamicInvocationResolveResult with no symbol, so the brackets carried no tooltip. Synthesize an indexer (FakeProperty, IsIndexer) on the target type with the index parameters typed from the callsite delegate, and attach it. Route it hover-only by detecting a DynamicInvocationResolveResult directly on the node - which also covers an invoke-member's own parentheses, so those stop producing a dead navigation link too. Assisted-by: Claude:claude-fable-5:Claude Code
1 parent cba8b0d commit a926ae7

4 files changed

Lines changed: 72 additions & 2 deletions

File tree

ICSharpCode.Decompiler.Tests/Output/CSharpAmbienceTests.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,25 @@ public void DynamicInvokeMember()
355355
ambience.ConversionFlags = ConversionFlags.All & ~(ConversionFlags.ShowBody | ConversionFlags.PlaceReturnTypeAfterParameterList);
356356
Assert.That(ambience.ConvertSymbol(method), Is.EqualTo("public dynamic dynamic.Compute(int, string, dynamic)"));
357357
}
358+
[Test]
359+
public void DynamicIndexer()
360+
{
361+
// The shape ExpressionBuilder synthesizes for a[b]: an indexer with dynamic return, index
362+
// parameters typed from the callsite delegate, declared on the dynamic type. It carries no
363+
// accessors, which the ambience renders cleanly (no empty { } artifact).
364+
var indexer = new FakeProperty(compilation) {
365+
Name = "Item",
366+
IsIndexer = true,
367+
ReturnType = SpecialType.Dynamic,
368+
DeclaringType = SpecialType.Dynamic,
369+
Parameters = new IParameter[] { new DefaultParameter(compilation.FindType(KnownTypeCode.Int32), string.Empty) },
370+
};
371+
ambience.ConversionFlags = ConversionFlags.ShowReturnType | ConversionFlags.ShowParameterList;
372+
Assert.That(ambience.ConvertSymbol(indexer), Is.EqualTo("dynamic this[int]"));
373+
374+
ambience.ConversionFlags = ConversionFlags.All & ~(ConversionFlags.ShowBody | ConversionFlags.PlaceReturnTypeAfterParameterList);
375+
Assert.That(ambience.ConvertSymbol(indexer), Is.EqualTo("public dynamic dynamic.this[int]"));
376+
}
358377
#endregion
359378

360379
#region Test types

ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4348,7 +4348,8 @@ protected internal override TranslatedExpression VisitDynamicGetIndexInstruction
43484348
var arguments = TranslateDynamicArguments(inst.Arguments.Skip(1), inst.ArgumentInfo.Skip(1)).ToList();
43494349
return new IndexerExpression(target, arguments.Select(a => a.Expression))
43504350
.WithILInstruction(inst)
4351-
.WithRR(new DynamicInvocationResolveResult(target.ResolveResult, DynamicInvocationType.Indexing, arguments.Select(a => a.ResolveResult).ToArray()));
4351+
.WithRR(new DynamicInvocationResolveResult(target.ResolveResult, DynamicInvocationType.Indexing, arguments.Select(a => a.ResolveResult).ToArray(),
4352+
symbol: CreateDynamicIndexerSymbol(DynamicArgumentType(inst.ArgumentInfo[0]), inst.ArgumentInfo.Skip(1).ToArray())));
43524353
}
43534354

43544355
protected internal override TranslatedExpression VisitDynamicGetMemberInstruction(DynamicGetMemberInstruction inst, TranslationContext context)
@@ -4509,6 +4510,24 @@ IMethod CreateDynamicConstructorSymbol(IType declaringType, IReadOnlyList<CSharp
45094510
return constructor;
45104511
}
45114512

4513+
/// <summary>
4514+
/// Synthesizes the indexer for a dynamic index access (a[b]): an indexer on the target type whose
4515+
/// parameters are typed from the callsite delegate, so the brackets carry a hover tooltip.
4516+
/// </summary>
4517+
IMember CreateDynamicIndexerSymbol(IType declaringType, IReadOnlyList<CSharpArgumentInfo> argumentInfo)
4518+
{
4519+
var parameters = new IParameter[argumentInfo.Count];
4520+
for (int i = 0; i < argumentInfo.Count; i++)
4521+
parameters[i] = new DefaultParameter(DynamicArgumentType(argumentInfo[i]), argumentInfo[i].Name ?? string.Empty);
4522+
return new FakeProperty(compilation) {
4523+
Name = "Item",
4524+
IsIndexer = true,
4525+
ReturnType = SpecialType.Dynamic,
4526+
DeclaringType = declaringType,
4527+
Parameters = parameters,
4528+
};
4529+
}
4530+
45124531
IEnumerable<TranslatedExpression> TranslateDynamicArguments(IEnumerable<ILInstruction> arguments, IEnumerable<CSharpArgumentInfo> argumentInfo)
45134532
{
45144533
foreach (var (argument, info) in arguments.Zip(argumentInfo))
@@ -4575,7 +4594,8 @@ protected internal override TranslatedExpression VisitDynamicSetIndexInstruction
45754594
var value = new TranslatedExpression(arguments.Last());
45764595
var indexer = new IndexerExpression(target, arguments.SkipLast(1).Select(a => a.Expression))
45774596
.WithoutILInstruction()
4578-
.WithRR(new DynamicInvocationResolveResult(target.ResolveResult, DynamicInvocationType.Indexing, arguments.SkipLast(1).Select(a => a.ResolveResult).ToArray()));
4597+
.WithRR(new DynamicInvocationResolveResult(target.ResolveResult, DynamicInvocationType.Indexing, arguments.SkipLast(1).Select(a => a.ResolveResult).ToArray(),
4598+
symbol: CreateDynamicIndexerSymbol(DynamicArgumentType(inst.ArgumentInfo[0]), inst.ArgumentInfo.Skip(1).Take(inst.ArgumentInfo.Count - 2).ToArray())));
45794599
return Assignment(indexer, value).WithILInstruction(inst);
45804600
}
45814601

ICSharpCode.Decompiler/Output/TextTokenWriter.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,10 @@ static bool IsDynamicMemberReference(AstNode node)
159159
{
160160
if (node.Annotation<ResolveResult>() is CSharp.Resolver.DynamicMemberResolveResult)
161161
return true;
162+
// The node itself is a dynamic invocation/indexing (a.Method(b), a[b]): its parentheses/brackets
163+
// carry the synthesized member.
164+
if (node.Annotation<ResolveResult>() is CSharp.Resolver.DynamicInvocationResolveResult)
165+
return true;
162166
if (node.Slot?.Kind == Slots.TargetExpression && node.Parent is InvocationExpression
163167
&& node.Parent.Annotation<ResolveResult>() is CSharp.Resolver.DynamicInvocationResolveResult)
164168
return true;

ILSpy.Tests/Editor/HoverOnlyReferenceTests.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ public object Call(dynamic d)
4949
{
5050
return d.Compute(1, "two", d);
5151
}
52+
53+
public object Index(dynamic d)
54+
{
55+
return d[0];
56+
}
5257
}
5358

5459
/// <summary>
@@ -111,4 +116,26 @@ public async Task Dynamic_Invoke_Member_Hover_Renders_Its_Synthesized_Signature(
111116
"each argument is typed from the callsite: the constants keep their compile-time type, "
112117
+ "the dynamic argument stays dynamic");
113118
}
119+
120+
[AvaloniaTest]
121+
public async Task Dynamic_Index_Access_Is_A_HoverOnly_Indexer()
122+
{
123+
var (_, vm) = await TestHarness.BootAsync();
124+
await vm.OpenAssemblyAsync(typeof(DynamicMemberSample).Assembly.Location);
125+
var typeNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(
126+
"ILSpy.Tests",
127+
"ICSharpCode.ILSpy.Tests.TextView",
128+
"ICSharpCode.ILSpy.Tests.TextView.DynamicMemberSample");
129+
vm.AssemblyTreeModel.SelectNode(typeNode);
130+
var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync();
131+
132+
// The brackets of d[0] carry a synthesized indexer.
133+
var bracket = tab.References!.First(r => r.Reference is IProperty { IsIndexer: true });
134+
bracket.Kind.Should().Be(ReferenceMode.HoverOnly, "the synthesized indexer has no metadata to jump to");
135+
136+
var ambience = new CSharpAmbience {
137+
ConversionFlags = ConversionFlags.ShowReturnType | ConversionFlags.ShowParameterList,
138+
};
139+
ambience.ConvertSymbol((IProperty)bracket.Reference!).Should().Be("dynamic this[int]");
140+
}
114141
}

0 commit comments

Comments
 (0)