diff --git a/ICSharpCode.Decompiler.Tests/Output/CSharpAmbienceTests.cs b/ICSharpCode.Decompiler.Tests/Output/CSharpAmbienceTests.cs index 89cadd347..08632d998 100644 --- a/ICSharpCode.Decompiler.Tests/Output/CSharpAmbienceTests.cs +++ b/ICSharpCode.Decompiler.Tests/Output/CSharpAmbienceTests.cs @@ -323,6 +323,40 @@ namespace ICSharpCode.Decompiler.Tests.Output } #endregion + #region Dynamic (synthesized member) tests + [Test] + public void DynamicType() + { + ambience.ConversionFlags = ConversionFlags.None; + Assert.That(ambience.ConvertType(SpecialType.Dynamic), Is.EqualTo("dynamic")); + } + + [Test] + public void DynamicInvokeMember() + { + // The shape ExpressionBuilder synthesizes for a.Compute(1, "two", d): dynamic return, + // per-argument types from the callsite delegate (constants keep their compile-time type), + // declared on the dynamic type. + var method = new FakeMethod(compilation, SymbolKind.Method) { + Name = "Compute", + ReturnType = SpecialType.Dynamic, + DeclaringType = SpecialType.Dynamic, + Parameters = new IParameter[] { + new DefaultParameter(compilation.FindType(KnownTypeCode.Int32), string.Empty), + new DefaultParameter(compilation.FindType(KnownTypeCode.String), string.Empty), + new DefaultParameter(SpecialType.Dynamic, string.Empty), + }, + }; + ambience.ConversionFlags = ConversionFlags.ShowReturnType | ConversionFlags.ShowParameterList; + Assert.That(ambience.ConvertSymbol(method), Is.EqualTo("dynamic Compute(int, string, dynamic)")); + + // The same member as DecompilerTextView.BuildHoverContent renders it (full hover form): the + // unnamed synthetic parameters collapse to their types, so there is no dangling-name artifact. + ambience.ConversionFlags = ConversionFlags.All & ~(ConversionFlags.ShowBody | ConversionFlags.PlaceReturnTypeAfterParameterList); + Assert.That(ambience.ConvertSymbol(method), Is.EqualTo("public dynamic dynamic.Compute(int, string, dynamic)")); + } + #endregion + #region Test types #pragma warning disable 169, 67 diff --git a/ILSpy.Tests/Editor/HoverOnlyReferenceTests.cs b/ILSpy.Tests/Editor/HoverOnlyReferenceTests.cs index a17a605bc..756342be5 100644 --- a/ILSpy.Tests/Editor/HoverOnlyReferenceTests.cs +++ b/ILSpy.Tests/Editor/HoverOnlyReferenceTests.cs @@ -24,6 +24,9 @@ using Avalonia.VisualTree; using AwesomeAssertions; +using ICSharpCode.Decompiler.CSharp.OutputVisitor; +using ICSharpCode.Decompiler.Output; +using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.ILSpy.TextView; using ICSharpCode.ILSpy.TreeNodes; @@ -41,6 +44,11 @@ public class DynamicMemberSample { return d.Property; } + + public object Call(dynamic d) + { + return d.Compute(1, "two", d); + } } /// @@ -76,4 +84,31 @@ public class HoverOnlyReferenceTests navigated.Should().BeFalse("clicking a hover-only reference must not navigate"); view.LocalReferenceMarks.Should().BeEmpty("a hover-only reference must not highlight occurrences"); } + + [AvaloniaTest] + public async Task Dynamic_Invoke_Member_Hover_Renders_Its_Synthesized_Signature() + { + 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 'Compute' hover target is the member synthesized for the dynamic call d.Compute(1, "two", d). + var compute = tab.References! + .Select(r => r.Reference) + .OfType() + .First(m => m.Name == "Compute"); + + var ambience = new CSharpAmbience { + ConversionFlags = ConversionFlags.ShowReturnType | ConversionFlags.ShowParameterList, + }; + ambience.ConvertSymbol(compute).Should().Be( + "dynamic Compute(int, string, dynamic)", + "each argument is typed from the callsite: the constants keep their compile-time type, " + + "the dynamic argument stays dynamic"); + } }