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

Posted

技术标签:

【中文标题】Roslyn - 如何用多个节点替换多个节点?【英文标题】:Roslyn - How can I replace multiple nodes with multiple nodes each? 【发布时间】:2017-04-23 01:54:11 【问题描述】:

背景:

将 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&lt;TAttribute&gt; 是我为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);

我正在使用第二个,因为我需要用字段和属性节点替换每个属性节点。我需要对许多节点执行此操作,但没有允许一对多节点替换的ReplaceNodes 过载。我发现解决该重载的唯一方法是使用 foreach 循环,这似乎非常“必要”并且违背了 Roslyn API 的功能感觉。

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


更新: 我在 Roslyn 上找到了一个很棒的博客系列并处理了它的不变性。我还没有找到确切的答案,但它看起来是一个很好的起点。 https://joshvarty.wordpress.com/learn-roslyn-now/


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

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

所以我的新node 实例不应该仍然包含在我的原始序列中找到的相同prop 实例吗?


特殊情况的解决方案:

我终于能够通过依赖属性标识符来解决转换属性声明的特殊问题,这在任何树转换中都不会改变。但是,我仍然想要一个用多个节点替换多个节点的通用解决方案。这个解决方案实际上是围绕 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 语句。这种情况显然不能依赖任何类型的唯一标识符,例如属性声明。

【问题讨论】:

您发送给ReplaceNodem 是谁?你确定它存在于node 中吗?为什么你不访问 jusrt PropertyDeclarationSyntax 对不起,这应该是prop,在我的原始源代码中它被称为m,但我将其更改为prop,以便在此处更具可读性。我不访问属性声明的原因是因为我需要用新的属性声明和字段声明替换属性声明。我不认为您可以在访问一个节点时将多个节点替换为一个节点,您需要在访问父节点时这样做(类型声明)。 我相信Replace方法的使用存在错误。我从Replace 得到的结果没有应用任何更改。我相信由于复制了不可变的数据结构,我引用了与创建 markedProperties 枚举时不同的树。当我的树引用不断变化时,我无法弄清楚如何进行这样的迭代替换。 【参考方案1】:

当你这样做时:

 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,您可以对文档进行多项更改,然后返回一个新文档。

如果您需要其中之一的示例,请告诉我。

【讨论】:

我在考虑第一个选项中的构建器之类的东西。我会从一棵空树开始,然后从原始树中添加节点(或处理过的节点),直到副本再次成为一棵完整的树吗?我以前没听说过SyntaxAnnotation;看起来很有希望。至于DocumentStackExchange.Precompilation钩入编译器的方式以CSharpSyntaxRewriter为中心。使用Document 是否需要更广泛的范围? 我刚找到你的博客;很有帮助的东西。我最近才开始深入研究 Roslyn 和 Cecil。 @JamesFaix 谢谢:) 你可以看到如何使用我用来转换 LINQ 的第一种方法的示例。 SyntaxAnnotions 和 TrackNodes 非常容易使用。我相信您可以在Josh Varty blog 上找到有用的信息。如果您有任何问题,请在此处或在新问题中提问。 使用DocumentEditor 对树进行多次更改

以上是关于Roslyn - 如何用多个节点替换多个节点?的主要内容,如果未能解决你的问题,请参考以下文章

当我们需要生成模板输出工件时,我们如何用 Roslyn 替换 T4

如何用python实现网络图节点权重的添加以及如何把一个非连通的大网络图分成多个小网络图

如何用一个值替换多个值python

XML 和 .NET:如何用从原始 xml 数据加载的许多其他节点替换特定节点

多个工作节点上的 Django + Celery 任务

如何用 Python 构建一个简单的分布式系统