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

时间:2016-12-07 23:09:55

标签: c# functional-programming abstract-syntax-tree roslyn

背景

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

我想转此:

[SpecialAttribute]
int AutoImplemented { get; set; }

进入这个:

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

private int _autoImplemented;

问题:

我已经能够进行简单的转换,但我仍然坚持使用自动属性,以及其他一些在某些方面相似的转换。我遇到的麻烦是在替换树中的多个节点时正确使用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循环,这似乎是非常“必要的”#39;并且反对Roslyn API的功能感。

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

更新 我找到了一篇关于Roslyn的精彩博客系列,并讨论了它的不变性。我还没有找到确切的答案,但它看起来是个好地方。 https://joshvarty.wordpress.com/learn-roslyn-now/

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

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

我的新node实例是否仍然包含在原始序列中找到的相同prop个实例?

针对特殊情况的Hacky解决方案:

我终于能够通过依赖属性标识符来解决这个特殊的转换属性声明的问题,这些标识符在任何树转换中都不会改变。但是,我仍然想要一个通用的解决方案,用每个节点替换多个节点。这个解决方案实际上是在解决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 个答案:

答案 0 :(得分: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中,一切都是不可变的,所以第一个替换应该适合你,并且你有一个新的树\节点。

要使其正常工作,您可以考虑以下其中一项:

  • 在您的重写器类中构建结果,而不更改原始树,并在完成后立即替换所有结果。在您的情况下,它的意思是立即替换class注释。当你想要替换语句时我认为它是一个很好的选择(我在编写代码时将它用于将linq查询(理解)转换为流利的语法)但是对于所有类,可能它并不是最优的。
  • 使用SyntaxAnnotaion \ TrackNodes在树更改后查找节点。使用这些选项,您可以根据需要更改树,您仍然可以跟踪新树中的旧节点。
  • 使用DocumentEditor,您可以对文档进行多项更改,然后返回新文档。

如果你需要其中一个例子,请告诉我。