Skip to content
53 changes: 53 additions & 0 deletions ICSharpCode.Decompiler.Tests/Output/CSharpAmbienceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,59 @@ public void DestructorTests(ConversionFlags flags, string expectedOutput)
}
#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)"));
}
[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
#pragma warning disable 169, 67

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ public void WriteReference(IMember member, string text, bool isDefinition = fals
MemberReferences.Add((text, member));
}

public void WriteLocalReference(string text, object reference, bool isDefinition = false)
public void WriteLocalReference(string text, object reference, bool isDefinition = false, bool isHoverOnly = false)
{
LocalReferences.Add((text, reference, isDefinition));
}
Expand Down
117 changes: 110 additions & 7 deletions ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4348,25 +4348,27 @@ protected internal override TranslatedExpression VisitDynamicGetIndexInstruction
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)
{
var target = TranslateDynamicTarget(inst.Target, inst.TargetArgumentInfo);
return new MemberReferenceExpression(target, inst.Name!)
.WithILInstruction(inst)
.WithRR(new DynamicMemberResolveResult(target.ResolveResult, inst.Name));
.WithRR(new DynamicMemberResolveResult(target.ResolveResult, inst.Name, CreateDynamicMemberSymbol(inst.Name!, inst.TargetArgumentInfo)));
}

protected internal override TranslatedExpression VisitDynamicInvokeConstructorInstruction(DynamicInvokeConstructorInstruction inst, TranslationContext context)
{
if (!(inst.ArgumentInfo[0].HasFlag(CSharpArgumentInfoFlags.IsStaticType) && IL.Transforms.TransformExpressionTrees.MatchGetTypeFromHandle(inst.Arguments[0], out var constructorType)))
return ErrorExpression("Could not detect static type for DynamicInvokeConstructorInstruction");
var arguments = TranslateDynamicArguments(inst.Arguments.Skip(1), inst.ArgumentInfo.Skip(1)).ToList();
//var names = inst.ArgumentInfo.Skip(1).Select(a => a.Name).ToArray();
var constructor = CreateDynamicConstructorSymbol(constructorType, inst.ArgumentInfo.Skip(1).ToArray());
return new ObjectCreateExpression(ConvertType(constructorType), arguments.Select(a => a.Expression))
.WithILInstruction(inst).WithRR(new ResolveResult(constructorType));
.WithILInstruction(inst)
.WithRR(new CSharpInvocationResolveResult(new ResolveResult(constructorType), constructor, arguments.Select(a => a.ResolveResult).ToArray()));
}

protected internal override TranslatedExpression VisitDynamicInvokeMemberInstruction(DynamicInvokeMemberInstruction inst, TranslationContext context)
Expand All @@ -4385,7 +4387,7 @@ protected internal override TranslatedExpression VisitDynamicInvokeMemberInstruc
var arguments = TranslateDynamicArguments(inst.Arguments.Skip(1), inst.ArgumentInfo.Skip(1)).ToList();
return new InvocationExpression(targetExpr, arguments.Select(a => a.Expression))
.WithILInstruction(inst)
.WithRR(new DynamicInvocationResolveResult(target.ResolveResult, DynamicInvocationType.Invocation, arguments.Select(a => a.ResolveResult).ToArray()));
.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)));
}

protected internal override TranslatedExpression VisitDynamicInvokeInstruction(DynamicInvokeInstruction inst, TranslationContext context)
Expand Down Expand Up @@ -4426,6 +4428,106 @@ TranslatedExpression TranslateDynamicTarget(ILInstruction inst, CSharpArgumentIn
return translatedTarget;
}

/// <summary>
/// The type to show for a dynamic callsite argument: its recorded compile-time type when the runtime
/// binder was told to use it (statically-typed or constant arguments), otherwise <c>dynamic</c>.
/// </summary>
static IType DynamicArgumentType(CSharpArgumentInfo info)
{
if ((info.HasFlag(CSharpArgumentInfoFlags.UseCompileTimeType) || info.HasFlag(CSharpArgumentInfoFlags.Constant))
&& info.CompileTimeType != null)
{
return info.CompileTimeType;
}
return SpecialType.Dynamic;
}

/// <summary>
/// Synthesizes a member on the target type for a dynamic member access (a.Member), so the member
/// reference carries a navigable symbol / hover tooltip. Since the real member is unknown, this is a
/// <c>dynamic</c>-typed field named after the accessed member, declared on the target's compile-time type.
/// </summary>
IMember CreateDynamicMemberSymbol(string name, CSharpArgumentInfo targetInfo)
{
return new FakeField(compilation) {
Name = name,
ReturnType = SpecialType.Dynamic,
DeclaringType = DynamicArgumentType(targetInfo),
};
}

/// <summary>
/// Synthesizes a member on the target type for a dynamic member invocation (a.Method(b)): a
/// <c>dynamic</c>-returning method named after the invoked member, with one parameter per argument
/// typed by <see cref="DynamicArgumentType"/>, so the member reference carries a navigable symbol /
/// hover tooltip.
/// </summary>
IMember CreateDynamicInvokeMemberSymbol(string name, CSharpArgumentInfo targetInfo, IReadOnlyList<CSharpArgumentInfo> argumentInfo, IReadOnlyList<IType> typeArguments)
{
var method = new FakeMethod(compilation, SymbolKind.Method) {
Name = name,
ReturnType = SpecialType.Dynamic,
DeclaringType = DynamicArgumentType(targetInfo),
};
if (argumentInfo.Count > 0)
{
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);
method.Parameters = parameters;
}
if (typeArguments.Count == 0)
return method;
// Explicit type arguments (a.Method<int>()): give the fake a matching set of type parameters,
// conventionally named T, T2, ..., and specialize it with the actual arguments like a real
// generic call would.
var typeParameters = new ITypeParameter[typeArguments.Count];
for (int i = 0; i < typeArguments.Count; i++)
typeParameters[i] = new DefaultTypeParameter(method, i, i == 0 ? "T" : "T" + (i + 1));
method.TypeParameters = typeParameters;
return SpecializedMethod.Create(method, new TypeParameterSubstitution(null, typeArguments));
}

/// <summary>
/// Synthesizes the constructor for a dynamic object creation (<c>new T(b)</c> where an argument is
/// dynamic): a constructor on the created type with one parameter per argument typed by
/// <see cref="DynamicArgumentType"/>, so the type name and parentheses carry a hover tooltip.
/// </summary>
IMethod CreateDynamicConstructorSymbol(IType declaringType, IReadOnlyList<CSharpArgumentInfo> argumentInfo)
{
var constructor = new FakeMethod(compilation, SymbolKind.Constructor) {
Name = ".ctor",
DeclaringType = declaringType,
ReturnType = compilation.FindType(KnownTypeCode.Void),
};
if (argumentInfo.Count > 0)
{
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);
constructor.Parameters = parameters;
}
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))
Expand Down Expand Up @@ -4492,7 +4594,8 @@ protected internal override TranslatedExpression VisitDynamicSetIndexInstruction
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);
}

Expand All @@ -4502,7 +4605,7 @@ protected internal override TranslatedExpression VisitDynamicSetMemberInstructio
var value = TranslateDynamicArgument(inst.Value, inst.ValueArgumentInfo);
var member = new MemberReferenceExpression(target, inst.Name!)
.WithoutILInstruction()
.WithRR(new DynamicMemberResolveResult(target.ResolveResult, inst.Name));
.WithRR(new DynamicMemberResolveResult(target.ResolveResult, inst.Name, CreateDynamicMemberSymbol(inst.Name!, inst.TargetArgumentInfo)));
return Assignment(member, value).WithILInstruction(inst);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,20 @@ public class DynamicInvocationResolveResult : ResolveResult
/// </summary>
public readonly IList<ResolveResult> InitializerStatements;

public DynamicInvocationResolveResult(ResolveResult target, DynamicInvocationType invocationType, IList<ResolveResult> arguments, IList<ResolveResult> initializerStatements = null) : base(SpecialType.Dynamic)
/// <summary>
/// Synthesized member (a <c>dynamic</c> method on the <c>dynamic</c> type) representing the invoked
/// member, so the member reference carries a navigable symbol / hover tooltip. Only set for an
/// invoke-member (<c>a.Method(b)</c>); null for a plain invoke or an indexer. May be null.
/// </summary>
public readonly IMember Symbol;

public DynamicInvocationResolveResult(ResolveResult target, DynamicInvocationType invocationType, IList<ResolveResult> arguments, IList<ResolveResult> initializerStatements = null, IMember symbol = null) : base(SpecialType.Dynamic)
{
this.Target = target;
this.InvocationType = invocationType;
this.Arguments = arguments ?? EmptyList<ResolveResult>.Instance;
this.InitializerStatements = initializerStatements ?? EmptyList<ResolveResult>.Instance;
this.Symbol = symbol;
}

public override string ToString()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,17 @@ public class DynamicMemberResolveResult : ResolveResult
/// </summary>
public readonly string Member;

public DynamicMemberResolveResult(ResolveResult target, string member) : base(SpecialType.Dynamic)
/// <summary>
/// Synthesized member (a <c>dynamic</c> field on the <c>dynamic</c> type) representing the accessed
/// member, so the member reference carries a navigable symbol / hover tooltip. May be null.
/// </summary>
public readonly IMember Symbol;

public DynamicMemberResolveResult(ResolveResult target, string member, IMember symbol = null) : base(SpecialType.Dynamic)
{
this.Target = target;
this.Member = member;
this.Symbol = symbol;
}

public override string ToString()
Expand Down
2 changes: 1 addition & 1 deletion ICSharpCode.Decompiler/Output/ITextOutput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public interface ITextOutput
void WriteReference(MetadataFile metadata, Handle handle, string text, string protocol = "decompile", bool isDefinition = false);
void WriteReference(IType type, string text, bool isDefinition = false);
void WriteReference(IMember member, string text, bool isDefinition = false);
void WriteLocalReference(string text, object reference, bool isDefinition = false);
void WriteLocalReference(string text, object reference, bool isDefinition = false, bool isHoverOnly = false);

void MarkFoldStart(string collapsedText = "...", bool defaultCollapsed = false, bool isDefinition = false);
void MarkFoldEnd();
Expand Down
4 changes: 2 additions & 2 deletions ICSharpCode.Decompiler/Output/PlainTextOutput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ public void WriteReference(IMember member, string text, bool isDefinition = fals
Write(text);
}

public void WriteLocalReference(string text, object reference, bool isDefinition = false)
public void WriteLocalReference(string text, object reference, bool isDefinition = false, bool isHoverOnly = false)
{
Write(text);
}
Expand Down Expand Up @@ -224,7 +224,7 @@ public void WriteLine()
actions.Add(target => target.WriteLine());
}

public void WriteLocalReference(string text, object reference, bool isDefinition = false)
public void WriteLocalReference(string text, object reference, bool isDefinition = false, bool isHoverOnly = false)
{
actions.Add(target => target.WriteLocalReference(text, reference, isDefinition));
}
Expand Down
47 changes: 45 additions & 2 deletions ICSharpCode.Decompiler/Output/TextTokenWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,17 @@ public override void WriteIdentifier(Identifier identifier)
output.WriteReference(t, name, false);
return;
case IMember m:
output.WriteReference(m, name, false);
if (IsDynamicMemberReference(nodeStack.Peek()))
{
// A member synthesized for a dynamic access: show its signature on hover, but do not
// make it a navigation target (there is no real member to jump to), and no occurrence
// highlight either - it is a distinct synthetic member at every use.
output.WriteLocalReference(name, m, isHoverOnly: true);
}
else
{
output.WriteReference(m, name, false);
}
return;
}

Expand Down Expand Up @@ -140,6 +150,31 @@ ISymbol FilterMember(ISymbol symbol)
return symbol;
}

/// <summary>
/// True if the member reference at this node was synthesized for a dynamic member access/invocation.
/// Such members carry a hover tooltip but must not be navigation targets, since they do not exist in
/// metadata.
/// </summary>
static bool IsDynamicMemberReference(AstNode node)
{
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;
// new T(dynamicArg): the object creation (or its type-name slot) is backed by a dynamic newobj,
// whose synthesized constructor has no metadata to navigate to.
if (node.Annotation<IL.DynamicInvokeConstructorInstruction>() != null)
return true;
if (node.Slot?.Kind == Slots.Type && node.Parent?.Annotation<IL.DynamicInvokeConstructorInstruction>() != null)
return true;
return false;
}

object GetCurrentLocalReference()
{
AstNode node = nodeStack.Peek();
Expand Down Expand Up @@ -319,7 +354,10 @@ public override void WriteToken(string token)
output.WriteReference(t, token, false);
return;
case IMember m:
output.WriteReference(m, token, false);
if (IsDynamicMemberReference(node))
output.WriteLocalReference(token, m, isHoverOnly: true);
else
output.WriteReference(m, token, false);
return;
}
}
Expand Down Expand Up @@ -422,6 +460,11 @@ public override void WritePrimitiveType(string type)
output.Write(type);
output.Write("()");
break;
case "dynamic":
// dynamic has no metadata type definition; emit it as a hover-only reference (no navigation
// target, no occurrence highlight) so it can still carry a tooltip.
output.WriteLocalReference(type, SpecialType.Dynamic, isHoverOnly: true);
return;
case "bool":
case "byte":
case "sbyte":
Expand Down
8 changes: 8 additions & 0 deletions ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -732,6 +732,14 @@ public static ISymbol GetSymbol(this ResolveResult rr)
{
return ((ConversionResolveResult)rr).Input.GetSymbol();
}
else if (rr is CSharp.Resolver.DynamicMemberResolveResult dynamicMember)
{
return dynamicMember.Symbol;
}
else if (rr is CSharp.Resolver.DynamicInvocationResolveResult dynamicInvocation)
{
return dynamicInvocation.Symbol;
}

return null;
}
Expand Down
Loading
Loading