From e60b6fc372de63b9dc375b8979634b41072c9770 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Sun, 20 Sep 2026 18:10:26 +0200 Subject: [PATCH 1/4] Look for extension methods innermost namespace first C# resolves an extension method one namespace at a time, starting at the namespace the call is written in and working outwards through the enclosing ones before it considers any import. A library that defines GetCustomAttributes(this MemberInfo) next to its callers therefore beats System.Reflection's identically shaped one, and the call compiles. ExpressionBuilder's resolver had a flat scope: the namespace superset was one undifferentiated list of imports, so the two competed as equals and the call looked ambiguous. The deleted AST transform did not have this problem because it nested its scope into the current type's namespace before resolving anything. The scope is memoized per namespace on the DecompileRun, because the set of extension methods a scope can reach is computed once per scope and an ExpressionBuilder exists per function. Assisted-by: Claude:claude-opus-5:Claude Code --- .../CSharp/ExpressionBuilder.cs | 2 +- ICSharpCode.Decompiler/DecompileRun.cs | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) 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/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)); From 9b759705a964044164ed36d54fd01234cfbef3a3 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Sun, 20 Sep 2026 18:34:31 +0200 Subject: [PATCH 2/4] Do not count the receiver as an argument of a method reference An extension method reference binds its receiver as the target, so the delegate it is converted to has one parameter fewer than the method. PerformOverloadResolution prepends the target itself when extension methods are allowed, so handing it the receiver's parameter as well left it with one argument too many: every candidate came back TooManyPositionalArguments, the reference was reported ambiguous, and the escalation ran to the end and wrote out type arguments that inference could have supplied. Two fixtures pinned that output. Both spellings compile to byte-identical IL - Use2(ints.Select) binds Enumerable.Select and the same Use2 overload as Use2(ints.Select), and .Any is the same Enumerable.Any as .Any inside the expression tree - so the shorter one is what the escalation is supposed to arrive at. Assisted-by: Claude:claude-opus-5:Claude Code --- .../TestCases/Pretty/DelegateConstruction.cs | 2 +- .../TestCases/Pretty/ExpressionTrees.cs | 2 +- ICSharpCode.Decompiler/CSharp/Disambiguator.cs | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) 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/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; From ffdbce6e35eb5cf3823d842fe3515d7731e9a171 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Sun, 20 Sep 2026 17:10:47 +0200 Subject: [PATCH 3/4] Pin extension method call syntax with a Pretty fixture Extension call syntax had no fixture of its own; it was covered only incidentally by QualifierTests, Issue1080, RefLocalsAndReturns and a few others. The cases that decide how the call is spelled - null receiver, ref/in receiver, a named argument after the receiver, type arguments inference cannot reach, params expansion, and an instance method that beats the extension so the call has to stay static - now have one place that fails if any of them moves. Assisted-by: Claude:claude-opus-5:Claude Code --- .../PrettyTestRunner.cs | 6 ++ .../TestCases/Pretty/ExtensionMethods.cs | 101 ++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 ICSharpCode.Decompiler.Tests/TestCases/Pretty/ExtensionMethods.cs 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/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) + { + } + } +} From 9b16bf3edb8a35cee6bdc675a04762d7021c049a Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Sun, 20 Sep 2026 17:32:58 +0200 Subject: [PATCH 4/4] Decide extension method syntax in CallBuilder Every other decision about how a member reference is spelled is made while the expression is built, behind Disambiguator, and CallBuilder already writes target.ExtensionMethod for delegate references and method groups. Extension calls were the last spelling decided afterwards, by an AST transform that had to rebuild a resolver from a UsingScope annotation on the tree root and re-derive every argument's resolve result from AST annotations. The check now runs against the resolver ExpressionBuilder already owns, whose using scope is the namespace superset the DecompileRun collected from the IL. That superset is wider than the imports actually printed, so it can only refuse infix syntax where the transform allowed it, never bind a call to a different overload. Two consequences had to be handled. The declaring type is no longer named anywhere once the call is infix, so IntroduceUsingDeclarations would not have imported its namespace and the output would not compile; it now imports from the invocation itself. And an extension GetEnumerator in infix form is spelled like an instance call, so the foreach matcher can no longer read a plain match as an instance method. Writing the receiver as the target means skipping an argument, which the argument list only supported where there were neither names nor optional arguments: it skipped before trimming the optional tail, so a skip past an omitted argument took one argument too many, and it gave up entirely on names. Both now skip after trimming, which also settles a latent off-by-one on the collection-initializer path. Assisted-by: Claude:claude-opus-5:Claude Code --- .../CSharp/CSharpDecompiler.cs | 3 +- ICSharpCode.Decompiler/CSharp/CallBuilder.cs | 121 +++++++++- .../CSharp/StatementBuilder.cs | 8 +- .../Transforms/IntroduceExtensionMethods.cs | 221 ------------------ .../Transforms/IntroduceUsingDeclarations.cs | 14 ++ doc/DecompilerArchitecture.html | 12 +- 6 files changed, 141 insertions(+), 238 deletions(-) delete mode 100644 ICSharpCode.Decompiler/CSharp/Transforms/IntroduceExtensionMethods.cs 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/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/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