Merge pull request #4160 from icsharpcode/refactor/extension-method-syntax-in-callbuilder

Decide extension method syntax in CallBuilder
This commit is contained in:
Siegfried Pammer
2026-09-29 07:45:01 +02:00
committed by GitHub
13 changed files with 281 additions and 242 deletions
@@ -1016,6 +1016,12 @@ namespace ICSharpCode.Decompiler.Tests
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)
{
@@ -529,7 +529,7 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty.DelegateConstruction
public static void ExtensionDelegateReference(IEnumerable<int> ints)
{
Use2(ints.Select<int, int>);
Use2(ints.Select);
}
#if CS70
@@ -512,7 +512,7 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
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()
@@ -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)
{
}
}
}
@@ -279,8 +279,7 @@ namespace ICSharpCode.Decompiler.CSharp
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(),
+113 -8
View File
@@ -66,13 +66,16 @@ namespace ICSharpCode.Decompiler.CSharp
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];
@@ -106,8 +109,8 @@ namespace ICSharpCode.Decompiler.CSharp
return Arguments
.SelectWithIndex(GetResolveResult)
.Skip(skipCount)
.Take(GetActualArgumentCount())
.Skip(skipCount)
.ToArray();
ResolveResult GetResolveResult(int index, TranslatedExpression expression)
@@ -122,28 +125,27 @@ namespace ICSharpCode.Decompiler.CSharp
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);
@@ -631,6 +633,16 @@ namespace ICSharpCode.Decompiler.CSharp
{
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 @@ namespace ICSharpCode.Decompiler.CSharp
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)
{
@@ -814,8 +814,12 @@ namespace ICSharpCode.Decompiler.CSharp
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;
@@ -100,7 +100,7 @@ namespace ICSharpCode.Decompiler.CSharp
this.compilation = decompilationContext.Compilation;
this.resolver = new CSharpResolver(new CSharpTypeResolveContext(
compilation.MainModule,
decompileRun.UsingScope,
decompileRun.GetUsingScopeFor(decompilationContext.CurrentTypeDefinition?.Namespace),
decompilationContext.CurrentTypeDefinition,
decompilationContext.CurrentMember
));
@@ -633,7 +633,9 @@ namespace ICSharpCode.Decompiler.CSharp
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 @@ namespace ICSharpCode.Decompiler.CSharp
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;
@@ -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
{
/// <summary>
/// Converts extension method calls into infix syntax.
/// </summary>
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<UsingScope>()!);
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<CSharpInvocationResolveResult>();
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<IType> typeArguments;
switch (invocationExpression.Target)
{
case MemberReferenceExpression mre:
typeArguments = mre.TypeArguments.Any() ? method.TypeArguments : EmptyList<IType>.Instance;
memberRefExpr = mre;
break;
case IdentifierExpression ide:
typeArguments = ide.TypeArguments.Any() ? method.TypeArguments : EmptyList<IType>.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);
}
}
}
@@ -146,6 +146,20 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
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<ForeachAnnotation>();
+25
View File
@@ -48,6 +48,31 @@ namespace ICSharpCode.Decompiler
public UsingScope UsingScope { get; }
readonly Dictionary<string, UsingScope> nestedUsingScopes = new Dictionary<string, UsingScope>();
/// <summary>
/// <see cref="UsingScope"/> as seen from inside <paramref name="namespaceName"/>.
/// 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.
/// </summary>
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));
+6 -6
View File
@@ -1476,8 +1476,7 @@ new DeclareVariables(), <span class="cm">// should run after most transforms tha
new TransformFieldAndConstructorInitializers(), <span class="cm">// must run after DeclareVariables</span>
new PrettifyAssignments(), <span class="cm">// must run after DeclareVariables</span>
new IntroduceUsingDeclarations(),
new IntroduceExtensionMethods(), <span class="cm">// must run after IntroduceUsingDeclarations</span>
new IntroduceQueryExpressions(), <span class="cm">// must run after IntroduceExtensionMethods</span>
new IntroduceQueryExpressions(), <span class="cm">// needs the extension method syntax CallBuilder writes</span>
new CombineQueryExpressions(),
new NormalizeBlockStatements(),
new FlattenSwitchBlocks(),
@@ -1522,10 +1521,11 @@ eventual imports to answer ambiguity questions &#8212; null propagation, for exa
<code>a != null ? Extensions.Method(a) : null</code> to <code>a?.Method()</code> 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
<code>DecompileRun</code>'s, and only this subset gets directives. Only after imports exist can
<strong><code>IntroduceExtensionMethods</code></strong> rewrite
<code>Enumerable.Where(xs, p)</code> to <code>xs.Where(p)</code> (an ordering that predates the
<code>DecompileRun</code> namespace superset), and only after <em>that</em> can
<code>DecompileRun</code>'s, and only this subset gets directives. Extension-method syntax &#8212; <code>Enumerable.Where(xs, p)</code> written as
<code>xs.Where(p)</code> &#8212; is not decided here: <code>CallBuilder</code> picks it while it
builds the call, resolving against the <code>DecompileRun</code> 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
<strong><code>IntroduceQueryExpressions</code></strong> rebuild <code>from&nbsp;&hellip;
select</code> query syntax (running the C# spec's query translation backwards), with
<strong><code>CombineQueryExpressions</code></strong> merging nested queries and dissolving the