Browse Source

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
pull/3907/head
Siegfried Pammer 2 months ago committed by Siegfried Pammer
parent
commit
a926ae7e8b
  1. 19
      ICSharpCode.Decompiler.Tests/Output/CSharpAmbienceTests.cs
  2. 24
      ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs
  3. 4
      ICSharpCode.Decompiler/Output/TextTokenWriter.cs
  4. 27
      ILSpy.Tests/Editor/HoverOnlyReferenceTests.cs

19
ICSharpCode.Decompiler.Tests/Output/CSharpAmbienceTests.cs

@ -355,6 +355,25 @@ namespace ICSharpCode.Decompiler.Tests.Output @@ -355,6 +355,25 @@ namespace ICSharpCode.Decompiler.Tests.Output
ambience.ConversionFlags = ConversionFlags.All & ~(ConversionFlags.ShowBody | ConversionFlags.PlaceReturnTypeAfterParameterList);
Assert.That(ambience.ConvertSymbol(method), Is.EqualTo("public dynamic dynamic.Compute(int, string, dynamic)"));
}
[Test]
public void DynamicIndexer()
{
// The shape ExpressionBuilder synthesizes for a[b]: an indexer with dynamic return, index
// parameters typed from the callsite delegate, declared on the dynamic type. It carries no
// accessors, which the ambience renders cleanly (no empty { } artifact).
var indexer = new FakeProperty(compilation) {
Name = "Item",
IsIndexer = true,
ReturnType = SpecialType.Dynamic,
DeclaringType = SpecialType.Dynamic,
Parameters = new IParameter[] { new DefaultParameter(compilation.FindType(KnownTypeCode.Int32), string.Empty) },
};
ambience.ConversionFlags = ConversionFlags.ShowReturnType | ConversionFlags.ShowParameterList;
Assert.That(ambience.ConvertSymbol(indexer), Is.EqualTo("dynamic this[int]"));
ambience.ConversionFlags = ConversionFlags.All & ~(ConversionFlags.ShowBody | ConversionFlags.PlaceReturnTypeAfterParameterList);
Assert.That(ambience.ConvertSymbol(indexer), Is.EqualTo("public dynamic dynamic.this[int]"));
}
#endregion
#region Test types

24
ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs

@ -4348,7 +4348,8 @@ namespace ICSharpCode.Decompiler.CSharp @@ -4348,7 +4348,8 @@ namespace ICSharpCode.Decompiler.CSharp
var arguments = TranslateDynamicArguments(inst.Arguments.Skip(1), inst.ArgumentInfo.Skip(1)).ToList();
return new IndexerExpression(target, arguments.Select(a => a.Expression))
.WithILInstruction(inst)
.WithRR(new DynamicInvocationResolveResult(target.ResolveResult, DynamicInvocationType.Indexing, arguments.Select(a => a.ResolveResult).ToArray()));
.WithRR(new DynamicInvocationResolveResult(target.ResolveResult, DynamicInvocationType.Indexing, arguments.Select(a => a.ResolveResult).ToArray(),
symbol: CreateDynamicIndexerSymbol(DynamicArgumentType(inst.ArgumentInfo[0]), inst.ArgumentInfo.Skip(1).ToArray())));
}
protected internal override TranslatedExpression VisitDynamicGetMemberInstruction(DynamicGetMemberInstruction inst, TranslationContext context)
@ -4509,6 +4510,24 @@ namespace ICSharpCode.Decompiler.CSharp @@ -4509,6 +4510,24 @@ namespace ICSharpCode.Decompiler.CSharp
return constructor;
}
/// <summary>
/// Synthesizes the indexer for a dynamic index access (a[b]): an indexer on the target type whose
/// parameters are typed from the callsite delegate, so the brackets carry a hover tooltip.
/// </summary>
IMember CreateDynamicIndexerSymbol(IType declaringType, IReadOnlyList<CSharpArgumentInfo> argumentInfo)
{
var parameters = new IParameter[argumentInfo.Count];
for (int i = 0; i < argumentInfo.Count; i++)
parameters[i] = new DefaultParameter(DynamicArgumentType(argumentInfo[i]), argumentInfo[i].Name ?? string.Empty);
return new FakeProperty(compilation) {
Name = "Item",
IsIndexer = true,
ReturnType = SpecialType.Dynamic,
DeclaringType = declaringType,
Parameters = parameters,
};
}
IEnumerable<TranslatedExpression> TranslateDynamicArguments(IEnumerable<ILInstruction> arguments, IEnumerable<CSharpArgumentInfo> argumentInfo)
{
foreach (var (argument, info) in arguments.Zip(argumentInfo))
@ -4575,7 +4594,8 @@ namespace ICSharpCode.Decompiler.CSharp @@ -4575,7 +4594,8 @@ namespace ICSharpCode.Decompiler.CSharp
var value = new TranslatedExpression(arguments.Last());
var indexer = new IndexerExpression(target, arguments.SkipLast(1).Select(a => a.Expression))
.WithoutILInstruction()
.WithRR(new DynamicInvocationResolveResult(target.ResolveResult, DynamicInvocationType.Indexing, arguments.SkipLast(1).Select(a => a.ResolveResult).ToArray()));
.WithRR(new DynamicInvocationResolveResult(target.ResolveResult, DynamicInvocationType.Indexing, arguments.SkipLast(1).Select(a => a.ResolveResult).ToArray(),
symbol: CreateDynamicIndexerSymbol(DynamicArgumentType(inst.ArgumentInfo[0]), inst.ArgumentInfo.Skip(1).Take(inst.ArgumentInfo.Count - 2).ToArray())));
return Assignment(indexer, value).WithILInstruction(inst);
}

4
ICSharpCode.Decompiler/Output/TextTokenWriter.cs

@ -159,6 +159,10 @@ namespace ICSharpCode.Decompiler @@ -159,6 +159,10 @@ namespace ICSharpCode.Decompiler
{
if (node.Annotation<ResolveResult>() is CSharp.Resolver.DynamicMemberResolveResult)
return true;
// The node itself is a dynamic invocation/indexing (a.Method(b), a[b]): its parentheses/brackets
// carry the synthesized member.
if (node.Annotation<ResolveResult>() is CSharp.Resolver.DynamicInvocationResolveResult)
return true;
if (node.Slot?.Kind == Slots.TargetExpression && node.Parent is InvocationExpression
&& node.Parent.Annotation<ResolveResult>() is CSharp.Resolver.DynamicInvocationResolveResult)
return true;

27
ILSpy.Tests/Editor/HoverOnlyReferenceTests.cs

@ -49,6 +49,11 @@ public class DynamicMemberSample @@ -49,6 +49,11 @@ public class DynamicMemberSample
{
return d.Compute(1, "two", d);
}
public object Index(dynamic d)
{
return d[0];
}
}
/// <summary>
@ -111,4 +116,26 @@ public class HoverOnlyReferenceTests @@ -111,4 +116,26 @@ public class HoverOnlyReferenceTests
"each argument is typed from the callsite: the constants keep their compile-time type, "
+ "the dynamic argument stays dynamic");
}
[AvaloniaTest]
public async Task Dynamic_Index_Access_Is_A_HoverOnly_Indexer()
{
var (_, vm) = await TestHarness.BootAsync();
await vm.OpenAssemblyAsync(typeof(DynamicMemberSample).Assembly.Location);
var typeNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(
"ILSpy.Tests",
"ICSharpCode.ILSpy.Tests.TextView",
"ICSharpCode.ILSpy.Tests.TextView.DynamicMemberSample");
vm.AssemblyTreeModel.SelectNode(typeNode);
var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync();
// The brackets of d[0] carry a synthesized indexer.
var bracket = tab.References!.First(r => r.Reference is IProperty { IsIndexer: true });
bracket.Kind.Should().Be(ReferenceMode.HoverOnly, "the synthesized indexer has no metadata to jump to");
var ambience = new CSharpAmbience {
ConversionFlags = ConversionFlags.ShowReturnType | ConversionFlags.ShowParameterList,
};
ambience.ConvertSymbol((IProperty)bracket.Reference!).Should().Be("dynamic this[int]");
}
}

Loading…
Cancel
Save