首页 文章

Roslyn - 如何用多个节点替换多个节点?

提问于
浏览
2

Background:

使用Roslyn和C#,我试图扩展自动实现的属性,以便访问器主体可以通过以后的处理注入代码 . 我使用StackExchange.Precompilation作为编译器钩子,因此这些语法转换发生在构建管道中,而不是作为分析器或重构的一部分 .

我想转此:

[SpecialAttribute]
int AutoImplemented { get; set; }

进入这个:

[SpecialAttribute]
int AutoImplemented {
    get { return _autoImplemented; }
    set { _autoImplemented = value; }
}

private int _autoImplemented;

The problem:

我已经能够使简单的转换工作,但是我正在使用 SyntaxNodeExtensions.ReplaceNodeSyntaxNodeExtensions.ReplaceNodes 扩展方法在替换树中的多个节点时正确使用 .

我正在使用一个扩展 CSharpSyntaxRewriter 的类来进行转换 . 我将在这里分享该课程的相关成员 . 此类访问每个 classstruct 声明,然后替换标有 SpecialAttribute 的所有属性声明 .

private readonly SemanticModel model;

public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node) {
    if (node == null) throw new ArgumentNullException(nameof(node));
    node = VisitMembers(node);
    return base.VisitClassDeclaration(node);
}

public override SyntaxNode VisitStructDeclaration(StructDeclarationSyntax node) {
    if (node == null) throw new ArgumentNullException(nameof(node));
    node = VisitMembers(node);
    return base.VisitStructDeclaration(node);
}

private TNode VisitMembers<TNode>(TNode node)
    where TNode : SyntaxNode {

    IEnumerable<PropertyDeclarationSyntax> markedProperties = 
        node.DescendantNodes()
            .OfType<PropertyDeclarationSyntax>()
            .Where(prop => prop.HasAttribute<SpecialAttribute>(model));

    foreach (var prop in markedProperties) {
        SyntaxList<SyntaxNode> expanded = ExpandProperty(prop);
        //If I set a breakpoint here, I can see that 'expanded' will hold the correct value.
        //ReplaceNode appears to not be replacing anything
        node = node.ReplaceNode(prop, expanded);
    }

    return node;
}

private SyntaxList<SyntaxNode> ExpandProperty(PropertyDeclarationSyntax node) {
    //Generates list of new syntax elements from original.
    //This method will produce correct output.
}

HasAttribute<TAttribute> 是我为 PropertyDeclarationSyntax 定义的扩展方法,它检查该属性是否具有给定类型的属性 . 此方法正常工作 .


我相信我没有正确使用 ReplaceNode . 有三种相关方法:

TRoot ReplaceNode<TRoot>(
    TRoot root,
    SyntaxNode oldNode,
    SyntaxNode newNode);

TRoot ReplaceNode<TRoot>(
    TRoot root,
    SyntaxNode oldNode,
    IEnumerable<SyntaxNode> newNodes);

TRoot ReplaceNodes<TRoot, TNode>(
    TRoot root, 
    IEnumerable<TNode> nodes, 
    Func<TNode, TNode, SyntaxNode> computeReplacementNode);

我正在使用第二个,因为我需要用field和property节点替换每个属性节点 . 我需要对许多节点执行此操作,但没有 ReplaceNodes 的重载允许一对多节点替换 . 我发现有这种重载的唯一方法是使用 foreach 循环,这似乎非常'imperative'并且与Roslyn API的功能感觉相反 .

有没有更好的方法来执行这样的批量转换?


Update: 我在Roslyn上发现了一个很棒的博客系列,并讨论了它的不变性 . 我还没有找到确切的答案,但它看起来像是一个好的开始 . https://joshvarty.wordpress.com/learn-roslyn-now/


Update: 所以这里是我真的很困惑的地方 . 我知道Roslyn API都是基于不可变数据结构,这里的问题是如何使用结构复制来模仿可变性的微妙之处 . 我认为问题在于,每次我替换树中的节点时,我都会有一个新树,所以当我调用 ReplaceNode 时,该树应该不包含我想要替换的原始节点 .

我的理解是,在Roslyn中复制树的方式是,当您替换树中的节点时,实际上创建了一个引用原始树的所有相同节点的新树,除了您替换的节点和上面的所有节点那个 . 如果替换节点不再引用它们,或者可以添加新引用,则可以移除被替换节点下面的节点,但是所有旧引用仍然指向与之前相同的节点实例 . 我很确定这正是Anders Hejlsberg在罗斯林的this interview中描述的(20到23分钟) .

那么我的新 node 实例是否应该包含在原始序列中找到的相同的 prop 实例?


Hacky solution for special cases:

我终于能够通过依赖属性标识符来转换属性声明来解决这个特殊问题,这在任何树转换中都不会改变 . 但是,我仍然想要一个通用的解决方案,用每个节点替换多个节点 . 这个解决方案实际上是在解决API而不是通过它 .

以下是特殊情况解决方案:

private TNode VisitMembers<TNode>(TNode node)
    where TNode : SyntaxNode {

    IEnumerable<PropertyDeclarationSyntax> markedPropertyNames = 
        node.DescendantNodes()
            .OfType<PropertyDeclarationSyntax>()
            .Where(prop => prop.HasAttribute<SpecialAttribute>(model))
            .Select(prop => prop.Identifier.ValueText);

    foreach (var prop in markedPropertyNames) {
        var oldProp = node.DescendantNodes()
            .OfType<PropertyDeclarationSyntax>()
            .Single(p => p.Identifier.ValueText == prop.Name);

        SyntaxList<SyntaxNode> newProp = ExpandProperty(oldProp);

        node = node.ReplaceNode(oldProp, newProp);
    }

    return node;
}

我正在使用的另一个类似问题是修改方法中的所有 return 语句以插入后置条件检查 . 这种情况显然不能依赖任何类型的唯一标识符,如属性声明 .

1 回答

  • 3

    当你这样做:

    foreach (var prop in markedProperties) {
        SyntaxList<SyntaxNode> expanded = ExpandProperty(prop);
        //If I set a breakpoint here, I can see that 'expanded' will hold the correct value.
        //ReplaceNode appears to not be replacing anything
        node = node.ReplaceNode(prop, expanded);
    }
    

    第一次替换后, node (例如您的 class )不再包含原始属性 .

    在Roslyn中,一切都是不可变的,所以第一个替换应该适合你,并且你有一个新的tree \ node .

    为了使其工作,您可以考虑以下之一:

    • 在不改变原始树的情况下在重写器类中构建结果,并在完成时立即替换所有结果 . 在您的情况下,它的意思是立即替换 class 注释 . 当你想要替换语句时我认为它是一个很好的选择(当我编写代码将linq查询(理解)转换为流利的语法时我使用它)但是对于所有类,也许它不是最优的 .

    • 使用SyntaxAnnotaion \ TrackNodes在树更改后查找节点 . 使用这些选项,您可以根据需要更改树,您仍然可以跟踪新树中的旧节点 .

    • 使用DocumentEditor,您可以对文档进行多项更改,然后返回新文档 .

    如果您需要其中一个示例,请告诉我 .

相关问题