diff --git a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs index db7f0e64b94..f5d4d502388 100644 --- a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs @@ -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) { diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DelegateConstruction.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DelegateConstruction.cs index 5863d6af5df..956b0284836 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DelegateConstruction.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DelegateConstruction.cs @@ -529,7 +529,7 @@ public static void DelegateReferenceWithStaticTarget() public static void ExtensionDelegateReference(IEnumerable ints) { - Use2(ints.Select); + Use2(ints.Select); } #if CS70 diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ExpressionTrees.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ExpressionTrees.cs index 6cd2ae3125f..07460cab2b2 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ExpressionTrees.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ExpressionTrees.cs @@ -493,7 +493,7 @@ private bool MyEquals(ExpressionTrees other) public void MethodGroupAsExtensionMethod() { - ToCode(X(), (Expression>>)(() => ((IEnumerable)new int[4] { 2000, 2004, 2008, 2012 }).Any)); + ToCode(X(), (Expression>>)(() => ((IEnumerable)new int[4] { 2000, 2004, 2008, 2012 }).Any)); } public void MethodGroupConstant() diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ExtensionMethods.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ExtensionMethods.cs new file mode 100644 index 00000000000..534529c1378 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ExtensionMethods.cs @@ -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 list) + { + list.FirstOrLast(last: true); + } + + public void NullReceiver() + { + ((string)null).Print(); + } + + public void ExplicitTypeArguments(object o) + { + o.As(); + } + + 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(this List list, bool last) + { + return list.Count; + } + + public static T As(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) + { + } + } +} diff --git a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs index 4e4b4fb60fd..1d2967e95d1 100644 --- a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs @@ -279,8 +279,7 @@ public static List 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(), diff --git a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs index 0af17049485..bad908d27f0 100644 --- a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs @@ -66,13 +66,16 @@ public int GetActualArgumentCount() return FirstOptionalArgumentIndex; } - public string[]? GetArgumentNames(int skipCount = 0) + /// + /// The name to write each argument with, indexed like ; null where + /// every argument is written positionally. + /// + 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]; @@ -106,8 +109,8 @@ public IList GetArgumentResolveResults(int skipCount = 0) return Arguments .SelectWithIndex(GetResolveResult) - .Skip(skipCount) .Take(GetActualArgumentCount()) + .Skip(skipCount) .ToArray(); ResolveResult GetResolveResult(int index, TranslatedExpression expression) @@ -122,28 +125,27 @@ ResolveResult GetResolveResult(int index, TranslatedExpression expression) public IList GetArgumentResolveResultsDirect(int skipCount = 0) { return Arguments - .Skip(skipCount) .Take(GetActualArgumentCount()) + .Skip(skipCount) .Select(a => a.ResolveResult) .ToArray(); } public IEnumerable 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); @@ -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); @@ -1267,6 +1279,99 @@ internal static bool IsOptionalArgument(IParameter parameter, TranslatedExpressi return object.Equals(parameter.GetConstantValue(), arg.ResolveResult.ConstantValue); } + /// + /// 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 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. + /// must be the one the call is being written with, because + /// whether the type arguments are spelled out decides which overloads the name reaches. + /// + 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.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) { diff --git a/ICSharpCode.Decompiler/CSharp/Disambiguator.cs b/ICSharpCode.Decompiler/CSharp/Disambiguator.cs index 447301faee6..e7d41f7f9ef 100644 --- a/ICSharpCode.Decompiler/CSharp/Disambiguator.cs +++ b/ICSharpCode.Decompiler/CSharp/Disambiguator.cs @@ -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; diff --git a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs index bfa376e8db8..680ea14f6ec 100644 --- a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs @@ -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 )); diff --git a/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs b/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs index 3385a2c2a9b..22b7dd75a53 100644 --- a/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs @@ -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; @@ -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; diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceExtensionMethods.cs b/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceExtensionMethods.cs deleted file mode 100644 index edde48815a4..00000000000 --- a/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceExtensionMethods.cs +++ /dev/null @@ -1,221 +0,0 @@ -// Copyright (c) 2011 AlphaSierraPapa for the SharpDevelop Team -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of this -// software and associated documentation files (the "Software"), to deal in the Software -// without restriction, including without limitation the rights to use, copy, modify, merge, -// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons -// to whom the Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all copies or -// substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR -// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE -// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -#nullable enable - -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Linq; - -using ICSharpCode.Decompiler.CSharp.Resolver; -using ICSharpCode.Decompiler.CSharp.Syntax; -using ICSharpCode.Decompiler.Semantics; -using ICSharpCode.Decompiler.TypeSystem; -using ICSharpCode.Decompiler.TypeSystem.Implementation; -using ICSharpCode.Decompiler.Util; - -namespace ICSharpCode.Decompiler.CSharp.Transforms -{ - /// - /// Converts extension method calls into infix syntax. - /// - public class IntroduceExtensionMethods : DepthFirstAstVisitor, IAstTransform - { - [AllowNull] - TransformContext context; - [AllowNull] - CSharpResolver resolver; - [AllowNull] - CSharpConversions conversions; - - public void Run(AstNode rootNode, TransformContext context) - { - this.context = context; - this.conversions = CSharpConversions.Get(context.TypeSystem); - // The decompiler attaches a UsingScope annotation to the syntax-tree root. - InitializeContext(rootNode.Annotation()!); - rootNode.AcceptVisitor(this); - } - - void InitializeContext(UsingScope usingScope) - { - if (!string.IsNullOrEmpty(context.CurrentTypeDefinition?.Namespace)) - { - foreach (string ns in context.CurrentTypeDefinition.Namespace.Split('.')) - { - usingScope = usingScope.WithNestedNamespace(ns); - } - } - var currentContext = new CSharpTypeResolveContext(context.TypeSystem.MainModule, usingScope, context.CurrentTypeDefinition); - this.resolver = new CSharpResolver(currentContext); - } - - public override void VisitNamespaceDeclaration(NamespaceDeclaration namespaceDeclaration) - { - var usingScope = resolver.CurrentUsingScope; - foreach (string ident in namespaceDeclaration.Identifiers) - { - usingScope = usingScope.WithNestedNamespace(ident); - } - var previousResolver = this.resolver; - try - { - this.resolver = this.resolver.WithCurrentUsingScope(usingScope); - base.VisitNamespaceDeclaration(namespaceDeclaration); - } - finally - { - this.resolver = previousResolver; - } - } - - public override void VisitTypeDeclaration(TypeDeclaration typeDeclaration) - { - var previousResolver = this.resolver; - this.resolver = resolver.WithCurrentTypeDefinition(typeDeclaration.GetSymbol() as ITypeDefinition); - try - { - base.VisitTypeDeclaration(typeDeclaration); - } - finally - { - this.resolver = previousResolver; - } - } - - public override void VisitInvocationExpression(InvocationExpression invocationExpression) - { - base.VisitInvocationExpression(invocationExpression); - if (!CanTransformToExtensionMethodCall(resolver, invocationExpression, out var memberRefExpr, - out var target, out var firstArgument)) - { - return; - } - var method = (IMethod)invocationExpression.GetSymbol()!; - bool stepped = false; - if (firstArgument is DirectionExpression dirExpr) - { - if (!context.Settings.RefExtensionMethods || dirExpr.FieldDirection == FieldDirection.Out) - return; - context.Step("Introduce extension method call", invocationExpression); - stepped = true; - // A ref/out direction expression always wraps an operand. - firstArgument = dirExpr.Expression!; - target = firstArgument.GetResolveResult(); - dirExpr.Detach(); - } - else if (firstArgument is NullReferenceExpression) - { - Debug.Assert(context.RequiredNamespacesSuperset.Contains(method.Parameters[0].Type.Namespace)); - context.Step("Introduce extension method call", invocationExpression); - stepped = true; - // The replacement is a freshly created CastExpression, so the result is non-null. - firstArgument = firstArgument.ReplaceWith(expr => new CastExpression(context.TypeSystemAstBuilder.ConvertType(method.Parameters[0].Type), expr.Detach()))!; - } - if (invocationExpression.Target is IdentifierExpression identifierExpression) - { - if (!stepped) - context.Step("Introduce extension method call", invocationExpression); - identifierExpression.Detach(); - memberRefExpr = new MemberReferenceExpression(firstArgument.Detach(), method.Name, identifierExpression.TypeArguments.Detach()); - invocationExpression.Target = memberRefExpr; - } - else - { - if (!stepped) - context.Step("Introduce extension method call", invocationExpression); - // The target is not an IdentifierExpression, so CanTransformToExtensionMethodCall - // matched the MemberReferenceExpression case and memberRefExpr is non-null. - memberRefExpr!.Target = firstArgument.Detach(); - } - if (invocationExpression.GetResolveResult() is CSharpInvocationResolveResult irr) - { - // do not forget to update the CSharpInvocationResolveResult => set IsExtensionMethodInvocation == true - invocationExpression.RemoveAnnotations(); - var newResolveResult = new CSharpInvocationResolveResult( - irr.TargetResult, irr.Member, irr.Arguments, irr.OverloadResolutionErrors, - isExtensionMethodInvocation: true, irr.IsExpandedForm, irr.IsDelegateInvocation, - irr.GetArgumentToParameterMap(), irr.InitializerStatements); - invocationExpression.AddAnnotation(newResolveResult); - } - } - - static bool CanTransformToExtensionMethodCall(CSharpResolver resolver, - InvocationExpression invocationExpression, out MemberReferenceExpression? memberRefExpr, - [NotNullWhen(true)] out ResolveResult? target, - [NotNullWhen(true)] out Expression? firstArgument) - { - var method = invocationExpression.GetSymbol() as IMethod; - memberRefExpr = null; - target = null; - firstArgument = null; - if (method == null || !method.IsExtensionMethod || !invocationExpression.Arguments.Any()) - return false; - IReadOnlyList typeArguments; - switch (invocationExpression.Target) - { - case MemberReferenceExpression mre: - typeArguments = mre.TypeArguments.Any() ? method.TypeArguments : EmptyList.Instance; - memberRefExpr = mre; - break; - case IdentifierExpression ide: - typeArguments = ide.TypeArguments.Any() ? method.TypeArguments : EmptyList.Instance; - memberRefExpr = null; - break; - default: - return false; - } - - firstArgument = invocationExpression.Arguments.First(); - if (firstArgument is NamedArgumentExpression) - return false; - target = firstArgument.GetResolveResult(); - if (target is ConstantResolveResult crr && crr.ConstantValue == null) - { - target = new ConversionResolveResult(method.Parameters[0].Type, crr, Conversion.NullLiteralConversion); - } - else if (firstArgument is DirectionExpression de) - { - target = de.Expression.GetResolveResult(); - } - Debug.Assert(target != null); - ResolveResult[] args = new ResolveResult[invocationExpression.Arguments.Count - 1]; - string[]? argNames = null; - int pos = 0; - foreach (var arg in invocationExpression.Arguments.Skip(1)) - { - if (arg is NamedArgumentExpression nae) - { - if (argNames == null) - { - argNames = new string[args.Length]; - } - argNames[pos] = nae.Name; - args[pos] = nae.Expression.GetResolveResult(); - } - else - { - args[pos] = arg.GetResolveResult(); - } - pos++; - } - return resolver.CanTransformToExtensionMethodCall(method, typeArguments, target, args, argNames); - } - } -} diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceUsingDeclarations.cs b/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceUsingDeclarations.cs index 230484c89b5..887f3a5297e 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceUsingDeclarations.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceUsingDeclarations.cs @@ -146,6 +146,20 @@ public override void VisitNamespaceDeclaration(NamespaceDeclaration namespaceDec currentNamespace = oldNamespace; } + public override void VisitInvocationExpression(InvocationExpression invocationExpression) + { + // A call written in extension method syntax no longer names its declaring type, + // so nothing else in the tree asks for that namespace to be imported - and + // without the import the name does not resolve back to this method. + if (invocationExpression.GetResolveResult() is CSharpInvocationResolveResult { + IsExtensionMethodInvocation: true, Member: var extensionMethod + }) + { + AddImportedNamespace(extensionMethod.DeclaringType); + } + base.VisitInvocationExpression(invocationExpression); + } + public override void VisitForeachStatement(ForeachStatement foreachStatement) { var annotation = foreachStatement.Annotation(); diff --git a/ICSharpCode.Decompiler/DecompileRun.cs b/ICSharpCode.Decompiler/DecompileRun.cs index 8f9242aefea..90475cc7bed 100644 --- a/ICSharpCode.Decompiler/DecompileRun.cs +++ b/ICSharpCode.Decompiler/DecompileRun.cs @@ -48,6 +48,31 @@ internal class DecompileRun public UsingScope UsingScope { get; } + readonly Dictionary nestedUsingScopes = new Dictionary(); + + /// + /// as seen from inside . + /// C# looks for extension methods one namespace at a time, innermost first, so a scope that + /// is not nested into the namespace the code is written in lets an extension method declared + /// there compete with merely imported ones instead of beating them. Memoized, because the + /// list of extension methods a scope can reach is built once per scope. + /// + public UsingScope GetUsingScopeFor(string namespaceName) + { + if (string.IsNullOrEmpty(namespaceName)) + return UsingScope; + if (!nestedUsingScopes.TryGetValue(namespaceName, out var scope)) + { + scope = UsingScope; + foreach (string part in namespaceName.Split('.')) + { + scope = scope.WithNestedNamespace(part); + } + nestedUsingScopes.Add(namespaceName, scope); + } + return scope; + } + public DecompileRun(DecompilerSettings settings, UsingScope usingScope) { this.Settings = settings ?? throw new ArgumentNullException(nameof(settings)); diff --git a/doc/DecompilerArchitecture.html b/doc/DecompilerArchitecture.html index 37d21b2b786..460de88c6e5 100644 --- a/doc/DecompilerArchitecture.html +++ b/doc/DecompilerArchitecture.html @@ -1476,8 +1476,7 @@

8.2 The AST transform pipeline

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(), @@ -1522,10 +1521,11 @@

8.2 The AST transform pipeline

a != null ? Extensions.Method(a) : null to a?.Method() only if it can guarantee the extension-method form will resolve unambiguously. Since many compiler-generated calls have disappeared by the time the AST exists, the AST-derived set is a subset of the -DecompileRun's, and only this subset gets directives. Only after imports exist can -IntroduceExtensionMethods rewrite -Enumerable.Where(xs, p) to xs.Where(p) (an ordering that predates the -DecompileRun namespace superset), and only after that can +DecompileRun's, and only this subset gets directives. Extension-method syntax — Enumerable.Where(xs, p) written as +xs.Where(p) — is not decided here: CallBuilder picks it while it +builds the call, resolving against the DecompileRun superset. The collector +therefore has to import the namespace of any call it finds in that form, because nothing in the +tree names the declaring type any more. Only then can IntroduceQueryExpressions rebuild from … select query syntax (running the C# spec's query translation backwards), with CombineQueryExpressions merging nested queries and dissolving the