如何用 SimpleLambdaExpression 中的另一个变量替换 lambda 参数的用法?

How do I replace usages of a lambda parameter with another variable in a SimpleLambdaExpression?

我是罗斯林的新手。我正在尝试编写一个分析器来检测 Select 何时在 foreach 循环中被迭代,例如

foreach (TResult item in source.Select(x => x.Foo()))
{
    ...
}

我正在编写一个代码修复提供程序,将此类语句转换为

foreach (TSource __ in source)
{
    TResult item = __.Foo();
    ...
}

这是我目前为代码修复提供程序提供的代码。 (它有点长;InlineSimpleLambdaExpressionAsync 是更改的主要内容,但我包含了 RegisterCodeFixesAsync 中的所有内容作为上下文。)

public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
    var syntaxRoot = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);

    var diagnostic = context.Diagnostics.First();
    var diagnosticSpan = diagnostic.Location.SourceSpan;

    var selectInvocation = (InvocationExpressionSyntax)syntaxRoot.FindNode(diagnosticSpan);
    var forEach = (ForEachStatementSyntax)selectInvocation.Parent;

    context.RegisterCodeFix(
        CodeAction.Create(
            title: Title,
            createChangedDocument: ct => InlineSelectorAsync(context.Document, forEach, selectInvocation, ct),
            equivalenceKey: Title),
        diagnostic);
}

private static Task<Document> InlineSelectorAsync(
    Document document,
    ForEachStatementSyntax forEach,
    InvocationExpressionSyntax selectInvocation,
    CancellationToken ct)
{
    var selectorExpr = selectInvocation.ArgumentList.Arguments.Single().Expression;

    switch (selectorExpr.Kind())
    {
        case SyntaxKind.SimpleLambdaExpression:
            // This will be the most common case.
            return InlineSimpleLambdaExpressionAsync(
                document,
                forEach,
                selectInvocation,
                (SimpleLambdaExpressionSyntax)selectorExpr,
                ct);
    }

    return Task.FromResult(document);
}

private static async Task<Document> InlineSimpleLambdaExpressionAsync(
    Document document,
    ForEachStatementSyntax forEach,
    InvocationExpressionSyntax selectInvocation,
    SimpleLambdaExpressionSyntax selectorExpr,
    CancellationToken ct)
{
    var smodel = await document.GetSemanticModelAsync(ct).ConfigureAwait(false);

    // First, change the foreach to iterate directly through the source enumerable,
    // and remove the Select() method call.
    // NOTE: GetSimpleMemberAccessExpression() is an extension method I wrote.
    var sourceExpr = selectInvocation.GetSimpleMemberAccessExpression()?.Expression;
    if (sourceExpr == null)
    {
        return document;
    }

    // Figure out the element type of the source enumerable.
    var sourceTypeSymbol = smodel.GetTypeInfo(sourceExpr, ct).Type;
    Debug.Assert(sourceTypeSymbol != null);
    // NOTE: GetElementType is an extension method I wrote.
    var elementTypeSymbol = sourceTypeSymbol.GetElementType(smodel);

    // Now, update the foreach. Replace the element type of the selected enumerable
    // with the element type of the source. Make '__' the identifier (TODO: Improve on this).
    var ident = SyntaxFactory.Identifier("__");
    int position = forEach.Type.SpanStart;
    var elementTypeSyntax = SyntaxFactory.IdentifierName(elementTypeSymbol.ToMinimalDisplayString(smodel, position));

    var newForEach = forEach
        .WithType(elementTypeSyntax)
        .WithIdentifier(ident)
        .WithExpression(sourceExpr);

    // Now, we have to take the selector and inline it.
    var selectorBody = selectorExpr.Body as ExpressionSyntax;
    Debug.Assert(selectorBody != null);
    var selectorParam = selectorExpr.Parameter;
    selectorBody = selectorBody.ReplaceNode(selectorParam, SyntaxFactory.IdentifierName("__")); // This doesn't work.
    var selectorStatement = SyntaxFactory.LocalDeclarationStatement(
        SyntaxFactory.VariableDeclaration(
            type: forEach.Type,
            variables: SyntaxFactory.SingletonSeparatedList(
                SyntaxFactory.VariableDeclarator(
                    identifier: forEach.Identifier,
                    argumentList: null,
                    initializer: SyntaxFactory.EqualsValueClause(selectorBody)))));

    var forEachStatment = forEach.Statement as BlockSyntax;
    // TODO: Consider supporting non-block statements? Would that happen with no braces?
    if (forEachStatment == null)
    {
        return document;
    }
    newForEach = newForEach.WithStatement(
        // NOTE: InsertStatements is an extension method I wrote.
        forEachStatment.InsertStatements(0, selectorStatement));

    // Update the syntax root and the document.
    var syntaxRoot = await document.GetSyntaxRootAsync(ct).ConfigureAwait(false);
    syntaxRoot = syntaxRoot.ReplaceNode(forEach, newForEach);
    return document.WithSyntaxRoot(syntaxRoot);
}

当我运行代码修复在下面的代码上:

foreach (var item in array.Select(x => x.ToString()))
{

}

我得到:

            foreach (int __ in array)
            {
var item = x.ToString();
            }

这(除了空格)几乎正是我想要的,只是我不知道如何用 __ 替换参数 x。特别是,这条线似乎不起作用:

selectorBody = selectorBody.ReplaceNode(selectorParam, SyntaxFactory.IdentifierName("__"));

我正在尝试将 SimpleLambdaExpressionBody 替换为它的 Parameter,但没有任何作用。我怀疑这行不通,但我还能如何在 lambda 中将 x 的用法替换为 __

您正在尝试替换 selectorBody 中缺少的 selectorParam 节点,因为在您的情况下,selectorBody 是对方法的调用 (x.ToString() - InvocationExpressionSyntax).可以通过如下重写代码得到替换:

var selectorBody = selectorExpr.Body as InvocationExpressionSyntax;
var nodeToReplace = (selectorBody.Expression as MemberAccessExpressionSyntax).Expression;
selectorBody = selectorBody.ReplaceNode(nodeToReplace, SyntaxFactory.IdentifierName("__"));

或者,如果你想依赖参数,那么你应该用下面的方式代替 SyntaxNode,而是 SyntaxTokens

var selectorBody = selectorExpr.Body as ExpressionSyntax;
var selectorParam = selectorExpr.Parameter.Identifier;
IEnumerable<SyntaxToken> tokensToReplace = selectorBody.DescendantTokens()
                                                       .Where(token => String.Equals(token.Text, selectorParam.Text));
selectorBody = selectorBody.ReplaceTokens(tokensToReplace, (t1, t2) => SyntaxFactory.IdentifierName("__").Identifier);