diff --git a/ICSharpCode.Decompiler.Tests/Output/CSharpAmbienceTests.cs b/ICSharpCode.Decompiler.Tests/Output/CSharpAmbienceTests.cs index 08632d998..b81065949 100644 --- a/ICSharpCode.Decompiler.Tests/Output/CSharpAmbienceTests.cs +++ b/ICSharpCode.Decompiler.Tests/Output/CSharpAmbienceTests.cs @@ -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 diff --git a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs index 1e364d5da..f2504ac9c 100644 --- a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs @@ -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 return constructor; } + /// + /// 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. + /// + IMember CreateDynamicIndexerSymbol(IType declaringType, IReadOnlyList 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 TranslateDynamicArguments(IEnumerable arguments, IEnumerable argumentInfo) { foreach (var (argument, info) in arguments.Zip(argumentInfo)) @@ -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); } diff --git a/ICSharpCode.Decompiler/Output/TextTokenWriter.cs b/ICSharpCode.Decompiler/Output/TextTokenWriter.cs index 2ea45c712..e9d796078 100644 --- a/ICSharpCode.Decompiler/Output/TextTokenWriter.cs +++ b/ICSharpCode.Decompiler/Output/TextTokenWriter.cs @@ -159,6 +159,10 @@ namespace ICSharpCode.Decompiler { if (node.Annotation() 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() is CSharp.Resolver.DynamicInvocationResolveResult) + return true; if (node.Slot?.Kind == Slots.TargetExpression && node.Parent is InvocationExpression && node.Parent.Annotation() is CSharp.Resolver.DynamicInvocationResolveResult) return true; diff --git a/ILSpy.Tests/Editor/HoverOnlyReferenceTests.cs b/ILSpy.Tests/Editor/HoverOnlyReferenceTests.cs index 756342be5..2f7fc75a8 100644 --- a/ILSpy.Tests/Editor/HoverOnlyReferenceTests.cs +++ b/ILSpy.Tests/Editor/HoverOnlyReferenceTests.cs @@ -49,6 +49,11 @@ public class DynamicMemberSample { return d.Compute(1, "two", d); } + + public object Index(dynamic d) + { + return d[0]; + } } /// @@ -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( + "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]"); + } }