Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1016,6 +1016,12 @@ public async Task DeconstructionTests([ValueSource(nameof(roslyn2OrNewerOptions)
await RunForLibrary(cscOptions: cscOptions);
}

[Test]
public async Task ExtensionMethods([ValueSource(nameof(roslyn2OrNewerOptions))] CompilerOptions cscOptions)
{
await RunForLibrary(cscOptions: cscOptions);
}

[Test]
public async Task CS9_ExtensionGetEnumerator([ValueSource(nameof(roslyn3OrNewerWithNet40Options))] CompilerOptions cscOptions)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,7 @@ public static void DelegateReferenceWithStaticTarget()

public static void ExtensionDelegateReference(IEnumerable<int> ints)
{
Use2(ints.Select<int, int>);
Use2(ints.Select);
}

#if CS70
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,7 @@ private bool MyEquals(ExpressionTrees other)

public void MethodGroupAsExtensionMethod()
{
ToCode(X(), (Expression<Func<Func<bool>>>)(() => ((IEnumerable<int>)new int[4] { 2000, 2004, 2008, 2012 }).Any<int>));
ToCode(X(), (Expression<Func<Func<bool>>>)(() => ((IEnumerable<int>)new int[4] { 2000, 2004, 2008, 2012 }).Any));
}

public void MethodGroupConstant()
Expand Down
101 changes: 101 additions & 0 deletions ICSharpCode.Decompiler.Tests/TestCases/Pretty/ExtensionMethods.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
using System;
using System.Collections.Generic;

namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
{
public class ExtensionMethods
{
public struct Value
{
public int Field;
}

public class HasInstanceMethod
{
public void Ambiguous(int i)
{
}
}

public void Simple(string text)
{
text.Print();
}

public void NamedArgumentAfterReceiver(List<int> list)
{
list.FirstOrLast(last: true);
}

public void NullReceiver()
{
((string)null).Print();
}

public void ExplicitTypeArguments(object o)
{
o.As<string>();
}

public void RefReceiver(Value value)
{
value.Increment();
}

public void InReceiver(Value value)
{
value.Read();
}

public void ParamsExpansion(string text)
{
text.Repeat(1, 2, 3);
}

public void InstanceMethodWinsSoTheCallStaysStatic(HasInstanceMethod x)
{
ExtensionMethodsProvider.Ambiguous(x, 1);
}

public Action MethodGroup(string text)
{
return text.Print;
}
}

public static class ExtensionMethodsProvider
{
public static void Print(this string text)
{
}

public static int FirstOrLast<T>(this List<T> list, bool last)
{
return list.Count;
}

public static T As<T>(this object o) where T : class
{
return o as T;
}

public static void Increment(this ref ExtensionMethods.Value value)
{
value.Field++;
}

public static int Read(this in ExtensionMethods.Value value)
{
return value.Field;
}

public static int Repeat(this string text, params int[] values)
{
return text.Length + values.Length;
}

public static void Ambiguous(this ExtensionMethods.HasInstanceMethod x, int i)
{
}
}
}
3 changes: 1 addition & 2 deletions ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -279,8 +279,7 @@ public static List<IAstTransform> GetAstTransforms()
new TransformFieldAndConstructorInitializers(), // must run after DeclareVariables
new PrettifyAssignments(), // must run after DeclareVariables
new IntroduceUsingDeclarations(),
new IntroduceExtensionMethods(), // must run after IntroduceUsingDeclarations
new IntroduceQueryExpressions(), // must run after IntroduceExtensionMethods
new IntroduceQueryExpressions(), // needs the extension method syntax CallBuilder writes
new CombineQueryExpressions(),
new NormalizeBlockStatements(),
new FlattenSwitchBlocks(),
Expand Down
121 changes: 113 additions & 8 deletions ICSharpCode.Decompiler/CSharp/CallBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,16 @@ public int GetActualArgumentCount()
return FirstOptionalArgumentIndex;
}

public string[]? GetArgumentNames(int skipCount = 0)
/// <summary>
/// The name to write each argument with, indexed like <see cref="Arguments"/>; null where
/// every argument is written positionally.
/// </summary>
public string[]? GetArgumentNames()
{
string[]? argumentNames = ArgumentNames;
if (AddNamesToPrimitiveValues && IsPrimitiveValue.Any() && !IsExpandedForm
&& !ParameterNames.Any(string.IsNullOrEmpty))
{
Debug.Assert(skipCount == 0);
if (argumentNames == null)
{
argumentNames = new string[Arguments.Length];
Expand Down Expand Up @@ -106,8 +109,8 @@ public IList<ResolveResult> GetArgumentResolveResults(int skipCount = 0)

return Arguments
.SelectWithIndex(GetResolveResult)
.Skip(skipCount)
.Take(GetActualArgumentCount())
.Skip(skipCount)
.ToArray();

ResolveResult GetResolveResult(int index, TranslatedExpression expression)
Expand All @@ -122,28 +125,27 @@ ResolveResult GetResolveResult(int index, TranslatedExpression expression)
public IList<ResolveResult> GetArgumentResolveResultsDirect(int skipCount = 0)
{
return Arguments
.Skip(skipCount)
.Take(GetActualArgumentCount())
.Skip(skipCount)
.Select(a => a.ResolveResult)
.ToArray();
}

public IEnumerable<Expression> GetArgumentExpressions(int skipCount = 0)
{
var argumentNames = GetArgumentNames(skipCount);
var argumentNames = GetArgumentNames();
int argumentCount = GetActualArgumentCount();
var useImplicitlyTypedOut = UseImplicitlyTypedOut;
if (argumentNames == null)
{
return Arguments.Skip(skipCount).Take(argumentCount).Select(arg => AddAnnotations(arg.Expression));
return Arguments.Take(argumentCount).Skip(skipCount).Select(arg => AddAnnotations(arg.Expression));
}
else
{
Debug.Assert(skipCount == 0);
// Zip stops at the shorter sequence, so names that ran short would silently drop
// the arguments past their end instead of leaving them unnamed.
Debug.Assert(argumentNames.Length == argumentCount);
return Arguments.Take(argumentCount).Zip(argumentNames,
return Arguments.Take(argumentCount).Skip(skipCount).Zip(argumentNames.Skip(skipCount),
(arg, name) => {
if (name == null)
return AddAnnotations(arg.Expression);
Expand Down Expand Up @@ -631,6 +633,16 @@ public ExpressionWithResolveResult Build(OpCode callOpCode, IMethod method,
{
argumentList.FirstOptionalArgumentIndex = -1;
}

if (TryUseExtensionMethodSyntax(foundMethod, transform, argumentList, out var extensionTarget,
out var extensionTargetResolveResult))
{
return new InvocationExpression(extensionTarget, argumentList.GetArgumentExpressions(skipCount: 1))
.WithRR(new CSharpInvocationResolveResult(extensionTargetResolveResult, foundMethod,
argumentList.GetArgumentResolveResultsDirect(skipCount: 1),
isExtensionMethodInvocation: true, isExpandedForm: argumentList.IsExpandedForm));
}

if ((transform & ReferenceTransformation.RequireTarget) != 0)
{
targetExpr = new MemberReferenceExpression(target.Expression, methodName);
Expand Down Expand Up @@ -1267,6 +1279,99 @@ internal static bool IsOptionalArgument(IParameter parameter, TranslatedExpressi
return object.Equals(parameter.GetConstantValue(), arg.ResolveResult.ConstantValue);
}

/// <summary>
/// Extension method syntax is the shortest spelling of a call to an extension method: the
/// first argument becomes the target and the rest stay arguments. It is only available
/// when the name resolves back to <paramref name="foundMethod"/> from that target, which a
/// competing instance method, another extension method in scope or an inaccessible
/// declaring type can all prevent - then the call is written as the static call it is in
/// IL and this returns false.
/// <paramref name="transform"/> must be the one the call is being written with, because
/// whether the type arguments are spelled out decides which overloads the name reaches.
/// </summary>
private bool TryUseExtensionMethodSyntax(IParameterizedMember foundMethod, ReferenceTransformation transform,
ArgumentList argumentList, [NotNullWhen(true)] out MemberReferenceExpression? memberRef,
[NotNullWhen(true)] out ResolveResult? targetResolveResult)
{
memberRef = null;
targetResolveResult = null;
// The overload the call resolves to, not the one the IL named: the two can differ in
// the type arguments inference substitutes, and the check below compares the candidate
// it finds against this one for equality.
if (foundMethod is not IMethod method)
return false;
// IsExtensionMethod is false unless settings.ExtensionMethods asked the type system
// for it, so it is the gate for the setting as well.
if (!method.IsExtensionMethod || argumentList.Length == 0)
return false;
// Without using declarations every type is named in full instead. An extension method
// has no such spelling: the namespace has to be imported for the name to be found.
if (!settings.UsingDeclarations)
return false;
// The target is the first argument, so it has to be written first and positionally.
var argumentNames = argumentList.GetArgumentNames();
if (argumentNames?[0] != null)
return false;
if (argumentList.ArgumentToParameterMap is { } map && map[0] != 0)
return false;
if (argumentList.FirstOptionalArgumentIndex == 0)
return false;

var firstArgument = argumentList.Arguments[0];
bool writeTypeArguments = (transform & ReferenceTransformation.RequireTypeArguments) != 0
&& (!settings.AnonymousTypes || !method.TypeArguments.Any(a => a.ContainsAnonymousType()));
IType[] typeArguments = writeTypeArguments ? method.TypeArguments.ToArray() : Empty<IType>.Array;

var directionExpression = firstArgument.Expression as DirectionExpression;
ResolveResult target = firstArgument.ResolveResult;
if (target is ConstantResolveResult { ConstantValue: null } nullLiteral)
{
// A null literal has no type of its own; the target type is the one the parameter
// gives it, which the cast below then writes out.
target = new ConversionResolveResult(method.Parameters[0].Type, nullLiteral,
Conversion.NullLiteralConversion);
}
else if (directionExpression != null)
{
if (!settings.RefExtensionMethods || directionExpression.FieldDirection == FieldDirection.Out)
return false;
target = directionExpression.Expression.GetResolveResult();
}

int actualArgumentCount = argumentList.GetActualArgumentCount();
string[]? remainingNames = argumentNames?.Take(actualArgumentCount).Skip(1).ToArray();
if (remainingNames != null && remainingNames.All(name => name == null))
remainingNames = null;
if (!resolver.CanTransformToExtensionMethodCall(method, typeArguments, target,
argumentList.GetArgumentResolveResults(skipCount: 1).ToArray(), remainingNames))
{
return false;
}

Expression targetExpression;
if (directionExpression != null)
{
// 'ref x.Ext()' is not a thing: the target carries the reference implicitly.
targetExpression = directionExpression.Expression.Detach();
}
else if (firstArgument.Expression is NullReferenceExpression)
{
targetExpression = new CastExpression(
expressionBuilder.ConvertType(method.Parameters[0].Type), firstArgument.Expression);
}
else
{
targetExpression = firstArgument.Expression;
}
memberRef = new MemberReferenceExpression(targetExpression, method.Name);
if (writeTypeArguments)
{
memberRef.TypeArguments.AddRange(method.TypeArguments.Select(expressionBuilder.ConvertType));
}
targetResolveResult = target;
return true;
}

private ReferenceTransformation GetRequiredTransformationsForCall(ExpectedTargetDetails expectedTargetDetails, IMethod method,
ref TranslatedExpression target, ref ArgumentList argumentList, ReferenceTransformation allowedTransforms, out IParameterizedMember? foundMethod)
{
Expand Down
6 changes: 5 additions & 1 deletion ICSharpCode.Decompiler/CSharp/Disambiguator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -814,8 +814,12 @@ bool IsUnambiguousMethodReference(ExpectedTargetDetails expectedTargetDetails, I
result = resolver.ResolveMemberAccess(target, method.Name, typeArguments, NameLookupMode.InvocationTarget) as MethodGroupResolveResult;
if (result == null)
return false;
// The receiver is the target, not an argument: the delegate being built has one
// parameter fewer than the method, so passing the receiver's parameter too leaves
// overload resolution with one argument too many and it reports every candidate
// ambiguous.
or = ((MethodGroupResolveResult)result).PerformOverloadResolution(resolver.CurrentTypeResolveContext.Compilation,
method.Parameters.SelectReadOnlyArray(p => new TypeResolveResult(p.Type)),
method.Parameters.Skip(1).Select(p => (ResolveResult)new TypeResolveResult(p.Type)).ToArray(),
argumentNames: null, allowExtensionMethods: true);
if (or == null || or.IsAmbiguous)
return false;
Expand Down
2 changes: 1 addition & 1 deletion ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public ExpressionBuilder(StatementBuilder statementBuilder, IDecompilerTypeSyste
this.compilation = decompilationContext.Compilation;
this.resolver = new CSharpResolver(new CSharpTypeResolveContext(
compilation.MainModule,
decompileRun.UsingScope,
decompileRun.GetUsingScopeFor(decompilationContext.CurrentTypeDefinition?.Namespace),
decompilationContext.CurrentTypeDefinition,
decompilationContext.CurrentMember
));
Expand Down
8 changes: 7 additions & 1 deletion ICSharpCode.Decompiler/CSharp/StatementBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -633,7 +633,9 @@ bool MatchGetEnumeratorPattern(Expression resource, out Match m, out bool isAsyn
m = getEnumeratorPattern.Match(resource);
if (!m.Success)
{
// ... or the extension GetEnumeratorPattern.
// ... or the extension GetEnumeratorPattern. CallBuilder writes the call this
// way when extension method syntax would not resolve back to the method, e.g.
// where two imported namespaces both offer a GetEnumerator extension.
m = extensionGetEnumeratorPattern.Match(resource);
if (!m.Success)
return false;
Expand All @@ -651,6 +653,10 @@ bool MatchGetEnumeratorPattern(Expression resource, out Match m, out bool isAsyn
m = getEnumeratorPattern.Match(resource);
if (!m.Success)
return false;
// An extension GetEnumerator written in extension method syntax is spelled like an
// instance call, so the pattern alone no longer tells them apart.
if (resource.GetSymbol() is IMethod { IsExtensionMethod: true })
return false;
}
isAsync = ((MemberReferenceExpression)((InvocationExpression)resource).Target).MemberName == "GetAsyncEnumerator";
return true;
Expand Down
Loading
Loading