From cba8b0d7a5ce7e0c9e3afe698885cb2e94f18eef Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 8 Jul 2026 18:05:08 +0200 Subject: [PATCH] Test dynamic member tooltip rendering Lock the hover content the dynamic-tooltip work produces. Ambience-level cases (CSharpAmbienceTests) pin that SpecialType.Dynamic renders as "dynamic" and that a synthetic dynamic method renders its return and per-argument types - including the full hover form, confirming the unnamed synthetic parameters collapse to their types with no dangling name. An end-to-end case (HoverOnlyReferenceTests) decompiles a dynamic call and renders the symbol GetSymbol hands back, exercising the actual synthesis (argument typing from the callsite delegate), not a hand-built stand-in. Assisted-by: Claude:claude-fable-5:Claude Code --- .../Output/CSharpAmbienceTests.cs | 34 ++++++++++++++++++ ILSpy.Tests/Editor/HoverOnlyReferenceTests.cs | 35 +++++++++++++++++++ 2 files changed, 69 insertions(+) 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"); + } }