From 8f66608d82348ca6bc1b9c724789f8305ca05440 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Tue, 6 Oct 2026 07:35:36 +0200 Subject: [PATCH 1/2] Improve dynamic reference tooltips Dynamic member tooltips should describe the visible static target, while punctuation should stay reserved for useful call targets such as constructors. This keeps dynamic object creation tooltips on the constructor parentheses but avoids noisy synthetic member references on dynamic invocation parentheses. Assisted-by: OpenAI:openai/gpt-5.5:OpenCode --- .../Output/TextTokenWriterTests.cs | 32 +++++++++++++++++-- .../CSharp/ExpressionBuilder.cs | 7 ++-- .../Output/TextTokenWriter.cs | 10 ++++-- .../CSharpHighlightingTokenWriter.cs | 4 ++- 4 files changed, 45 insertions(+), 8 deletions(-) diff --git a/ICSharpCode.Decompiler.Tests/Output/TextTokenWriterTests.cs b/ICSharpCode.Decompiler.Tests/Output/TextTokenWriterTests.cs index 20d3109f373..ab90064c84a 100644 --- a/ICSharpCode.Decompiler.Tests/Output/TextTokenWriterTests.cs +++ b/ICSharpCode.Decompiler.Tests/Output/TextTokenWriterTests.cs @@ -76,13 +76,22 @@ public T UseInGenericMethod(T t) } } + internal class DynamicInvokeMemberHost + { + public object DynamicStaticMethod(Type type) + { + dynamic dynamicType = type; + return Activator.CreateInstance(dynamicType); + } + } + [TestFixture] public class TextTokenWriterTests { sealed class ReferenceRecordingOutput : ITextOutput { public readonly List<(string Text, IMember Member)> MemberReferences = new(); - public readonly List<(string Text, object Reference, bool IsDefinition)> LocalReferences = new(); + public readonly List<(string Text, object Reference, bool IsDefinition, bool IsHoverOnly)> LocalReferences = new(); public readonly List FoldStartDefaultCollapsed = new(); public int FoldEndCount; @@ -104,7 +113,7 @@ public void WriteReference(IMember member, string text, bool isDefinition = fals public void WriteLocalReference(string text, object reference, bool isDefinition = false, bool isHoverOnly = false) { - LocalReferences.Add((text, reference, isDefinition)); + LocalReferences.Add((text, reference, isDefinition, isHoverOnly)); } public void MarkDefinitionStart() @@ -248,6 +257,25 @@ public void GenericLocalFunctionDefinitionAndUsesShareEqualReferenceObjects() }); } + [Test] + public void DynamicStaticInvocationReferencesStaticTargetTypeOnMemberNameOnly() + { + using var module = OpenTestAssembly(); + var output = DecompileAndCollectReferences(module, typeof(DynamicInvokeMemberHost)); + + var references = output.LocalReferences + .Where(r => r.Text == "CreateInstance") + .ToList(); + + Assert.That(references, Has.Count.EqualTo(1)); + Assert.That(references[0].IsHoverOnly, Is.True); + Assert.That(references[0].Reference, Is.InstanceOf()); + var member = (IMember)references[0].Reference; + Assert.That(member.DeclaringType.FullName, Is.EqualTo("System.Activator")); + Assert.That(member.Name, Is.EqualTo("CreateInstance")); + Assert.That(output.LocalReferences.Where(r => r.Text is "(" or ")"), Is.Empty); + } + // Runs inside Assert.Multiple: after a recorded assertion failure, bail out of the checks // that would dereference the missing data instead of throwing. static void AssertLocalFunctionReferenceGroup(ReferenceRecordingOutput output, string name, int expectedUses) diff --git a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs index 25a1a6c4492..8461e11f916 100644 --- a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs @@ -4660,9 +4660,10 @@ protected internal override TranslatedExpression VisitDynamicInvokeMemberInstruc } IEnumerable argumentValues = inst.StaticTargetType != null ? inst.Arguments : inst.Arguments.Skip(1); var arguments = TranslateDynamicArguments(argumentValues, inst.ArgumentInfo.Skip(1)).ToList(); + var symbolDeclaringType = inst.StaticTargetType ?? DynamicArgumentType(inst.ArgumentInfo[0]); return new InvocationExpression(targetExpr, arguments.Select(a => a.Expression)) .WithILInstruction(inst) - .WithRR(new DynamicInvocationResolveResult(target.ResolveResult, DynamicInvocationType.Invocation, arguments.Select(a => a.ResolveResult).ToArray(), symbol: CreateDynamicInvokeMemberSymbol(inst.Name, inst.ArgumentInfo[0], inst.ArgumentInfo.Skip(1).ToArray(), inst.TypeArguments))); + .WithRR(new DynamicInvocationResolveResult(target.ResolveResult, DynamicInvocationType.Invocation, arguments.Select(a => a.ResolveResult).ToArray(), symbol: CreateDynamicInvokeMemberSymbol(inst.Name, symbolDeclaringType, inst.ArgumentInfo.Skip(1).ToArray(), inst.TypeArguments))); } protected internal override TranslatedExpression VisitDynamicInvokeInstruction(DynamicInvokeInstruction inst, TranslationContext context) @@ -4737,12 +4738,12 @@ IMember CreateDynamicMemberSymbol(string name, CSharpArgumentInfo targetInfo) /// typed by , so the member reference carries a navigable symbol / /// hover tooltip. /// - IMember CreateDynamicInvokeMemberSymbol(string name, CSharpArgumentInfo targetInfo, IReadOnlyList argumentInfo, IReadOnlyList typeArguments) + IMember CreateDynamicInvokeMemberSymbol(string name, IType declaringType, IReadOnlyList argumentInfo, IReadOnlyList typeArguments) { var method = new FakeMethod(compilation, SymbolKind.Method) { Name = name, ReturnType = SpecialType.Dynamic, - DeclaringType = DynamicArgumentType(targetInfo), + DeclaringType = declaringType, }; if (argumentInfo.Count > 0) { diff --git a/ICSharpCode.Decompiler/Output/TextTokenWriter.cs b/ICSharpCode.Decompiler/Output/TextTokenWriter.cs index 424387b2185..61d4656e85b 100644 --- a/ICSharpCode.Decompiler/Output/TextTokenWriter.cs +++ b/ICSharpCode.Decompiler/Output/TextTokenWriter.cs @@ -124,7 +124,9 @@ ISymbol GetCurrentMemberReference() } if (symbol != null && node.Slot?.Kind == Slots.Type && node.Parent is ObjectCreateExpression) { - var ctorSymbol = node.Parent.GetSymbol(); + var ctorSymbol = node.Parent.Annotation()?.Method + ?? node.Parent.Annotation()?.Type.GetDefinition() + ?? node.Parent.GetSymbol(); if (ctorSymbol != null) symbol = ctorSymbol; } @@ -364,7 +366,11 @@ public override void WriteToken(string token) output.WriteReference(t, token, false); return; case IMember m: - if (IsDynamicMemberReference(node)) + if (node is InvocationExpression && node.Annotation() is DynamicInvocationResolveResult) + { + output.Write(token); + } + else if (IsDynamicMemberReference(node)) output.WriteLocalReference(token, m, isHoverOnly: true); else output.WriteReference(m, token, false); diff --git a/ILSpy/Languages/CSharpHighlightingTokenWriter.cs b/ILSpy/Languages/CSharpHighlightingTokenWriter.cs index 9b9f8529ff0..fe992d8b1c2 100644 --- a/ILSpy/Languages/CSharpHighlightingTokenWriter.cs +++ b/ILSpy/Languages/CSharpHighlightingTokenWriter.cs @@ -486,7 +486,9 @@ public override void WritePrimitiveValue(object? value, ICSharpCode.Decompiler.C } if (symbol != null && node.Slot?.Kind == Slots.Type && node.Parent is ObjectCreateExpression) { - var ctorSymbol = node.Parent.GetSymbol(); + var ctorSymbol = node.Parent.Annotation()?.Method + ?? node.Parent.Annotation()?.Type.GetDefinition() + ?? node.Parent.GetSymbol(); if (ctorSymbol != null) symbol = ctorSymbol; } From ef0b4668cca07b226e3a75a6d972bb0a201ca29a Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Tue, 6 Oct 2026 07:44:01 +0200 Subject: [PATCH 2/2] Return symbols for method-group resolve results Dynamic and token-writer reference paths can receive method-group resolve results directly. Returning the first method keeps ResolveResult.GetSymbol aligned with the existing method-group handling used by AST annotations. Assisted-by: OpenAI:openai/gpt-5.5:OpenCode --- .../TypeSystem/TypeSystemExtensions.cs | 40 ++++++++----------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs b/ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs index 3c879df6a1e..a8f23076d61 100644 --- a/ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs +++ b/ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs @@ -22,6 +22,7 @@ using System.Linq; using System.Reflection.Metadata; +using ICSharpCode.Decompiler.CSharp.Resolver; using ICSharpCode.Decompiler.IL.Transforms; using ICSharpCode.Decompiler.Metadata; using ICSharpCode.Decompiler.Semantics; @@ -796,29 +797,22 @@ public static ITypeDefinition GetTypeDefinition(this IModule module, string name #region ResolveResult public static ISymbol GetSymbol(this ResolveResult rr) { - if (rr is LocalResolveResult) - { - return ((LocalResolveResult)rr).Variable; - } - else if (rr is MemberResolveResult) - { - return ((MemberResolveResult)rr).Member; - } - else if (rr is TypeResolveResult) - { - return ((TypeResolveResult)rr).Type.GetDefinition(); - } - else if (rr is ConversionResolveResult) - { - return ((ConversionResolveResult)rr).Input.GetSymbol(); - } - else if (rr is DynamicMemberResolveResult dynamicMember) - { - return dynamicMember.Symbol; - } - else if (rr is DynamicInvocationResolveResult dynamicInvocation) - { - return dynamicInvocation.Symbol; + switch (rr) + { + case LocalResolveResult lrr: + return lrr.Variable; + case MemberResolveResult mrr: + return mrr.Member; + case TypeResolveResult trr: + return trr.Type.GetDefinition(); + case ConversionResolveResult crr: + return crr.Input.GetSymbol(); + case MethodGroupResolveResult mgrr: + return mgrr.Methods.FirstOrDefault(); + case DynamicMemberResolveResult dynamicMember: + return dynamicMember.Symbol; + case DynamicInvocationResolveResult dynamicInvocation: + return dynamicInvocation.Symbol; } return null;