【问题标题】:Code diff using Roslyn CTP API使用 Roslyn CTP API 的代码差异
【发布时间】:2011-11-30 01:30:28
【问题描述】:

我正在尝试使用 Roslyn API 进行一些基本的代码差异,但遇到了一些意想不到的问题。本质上,我有两段相同的代码,除了添加了一行。这应该只返回已更改文本的行,但由于某种原因,它告诉我一切都已更改。我也尝试过只编辑一行而不是添加一行,但我得到了相同的结果。我希望能够将此应用于源文件的两个版本,以识别两者之间的差异。这是我目前正在使用的代码:

        SyntaxTree tree = SyntaxTree.ParseCompilationUnit(
            @"using System;
            using System.Collections.Generic;
            using System.Linq;
            using System.Text;

            namespace HelloWorld
            {
                class Program
                {
                    static void Main(string[] args)
                    {
                        Console.WriteLine(""Hello, World!"");
                    }
                }
            }");

        var root = (CompilationUnitSyntax)tree.Root;

        var compilation = Compilation.Create("HelloWorld")
                                     .AddReferences(
                                        new AssemblyFileReference(
                                            typeof(object).Assembly.Location))
                                     .AddSyntaxTrees(tree);

        var model = compilation.GetSemanticModel(tree);
        var nameInfo = model.GetSemanticInfo(root.Usings[0].Name);
        var systemSymbol = (NamespaceSymbol)nameInfo.Symbol;

        SyntaxTree tree2 = SyntaxTree.ParseCompilationUnit(
            @"using System;
            using System.Collections.Generic;
            using System.Linq;
            using System.Text;

            namespace HelloWorld
            {
                class Program
                {
                    static void Main(string[] args)
                    {
                        Console.WriteLine(""Hello, World!"");
                        Console.WriteLine(""jjfjjf"");
                    }
                }
            }");

        var root2 = (CompilationUnitSyntax)tree2.Root;

        var compilation2 = Compilation.Create("HelloWorld")
                                     .AddReferences(
                                        new AssemblyFileReference(
                                            typeof(object).Assembly.Location))
                                     .AddSyntaxTrees(tree2);

        var model2 = compilation2.GetSemanticModel(tree2);
        var nameInfo2 = model2.GetSemanticInfo(root2.Usings[0].Name);
        var systemSymbol2 = (NamespaceSymbol)nameInfo2.Symbol;

        foreach (TextSpan t in tree2.GetChangedSpans(tree))
        {
            Console.WriteLine(tree2.Text.GetText(t));
        }

这是我得到的输出:

System
                using System
Collections
Generic
                using System
Linq
                using System
Text

                namespace HelloWorld
                {
                    class Program
                    {
                        static
Main
args
                        {
                            Console
WriteLine
"Hello, World!"
                            Console.WriteLine("jjfjjf");
                        }
                    }
                }
Press any key to continue . . .

有趣的是,它似乎将每一行显示为每一行的标记,除了添加的行,它显示该行而不分解它。有谁知道如何隔离实际更改?

【问题讨论】:

  • 你确定你不是在寻找一个简单的diff 工具吗?
  • 我希望将它保留在 Roslyn API 中,因为我正在使用它来设置回归测试,并且我认为将差异部分保留在 Roslyn 的 API 中会更容易。如果这不起作用,我可能最终会使用像 this one 这样的工具
  • 您对其他计算树差异的工具感兴趣吗?

标签: c# .net roslyn


【解决方案1】:

Bruce Boughton 的猜测是正确的。 GetChangedSpans 方法并非旨在作为通用语法差异机制来获取没有共享历史记录的两个语法树之间的差异。相反,它的目的是将通过编辑生成的两棵树用于同一棵树,并确定树的哪些部分因编辑而不同。

如果您采用了第一个解析树并将新语句作为编辑插入其中,那么您会看到一组更小的更改。

如果我简要描述一下 Roslyn 词法分析器和解析器的工作原理,可能会有所帮助。

基本思想是词法分析器生成的“语法标记”和解析器生成的“语法树”是不可变的。他们从不改变。因为它们永远不会改变,所以我们可以在新的解析树中重用之前解析树的部分内容。 (具有此属性的数据结构通常称为“持久”数据结构。)

因为我们可以重复使用现有部分,例如,我们可以对程序中出现的给定令牌的每个实例使用相同的值,例如 class。每个class令牌的长度和内容完全相同;区分两个不同的class 标记的唯一因素是它们的琐事(它们周围的间距和cmets)及其位置,以及它们的父级 -- 哪个更大的语法节点包含令牌。

当您解析文本块时,我们会以永久、不可变的形式生成语法标记和语法树,我们称之为“绿色”形式。然后我们将绿色节点包裹在“红色”层中。绿色层对位置、父母等一无所知。红色层可以。 (异想天开的名字是因为当我们第一次在白板上绘制这个数据结构时,这些是我们使用的颜色。)当您创建一个给定语法树的编辑时,我们查看之前的语法树,识别更改的节点,然后仅在更改的主干上构建新节点。绿树的所有其他分支都保持不变。

当区分两棵树时,基本上我们所做的就是取绿色节点的集合差。如果其中一棵树是通过编辑另一棵树生成的,那么几乎所有的绿色节点都将是相同的,因为只有脊椎被重建。树差异算法将识别更改的节点并计算出受影响的跨度。

如果两棵树没有共同的历史,那么它们唯一的共同绿色节点就是单独的令牌,正如我之前所说,它们在任何地方都可以重复使用。每个更高级别的绿色语法节点都是不同的绿色节点,因此即使其文本相同,也会被树差异引擎视为不同。

此方法的目的是允许编辑器代码快速保守地猜测文本缓冲区的哪些部分需要在编辑或撤消之后重新着色,或类似的东西。假设是这些树具有历史关系。目的不是提供通用的文本差异机制;已经有很多很棒的工具了。

例如,假设您已将第一个程序粘贴到编辑器中,然后突出显示整个内容,然后将第二个程序粘贴到编辑器中。人们会合理地期望编辑器不会浪费时间试图找出粘贴代码的哪些部分恰好与之前粘贴的代码相同。这可能非常昂贵,答案可能是“不多”。相反,编辑器做出保守的假设,即整个粘贴区域是全新的且完全不同的代码。它不会花费任何时间尝试在旧代码和新代码之间建立对应关系;它重新解析并因此重新着色整个事物。

另一方面,如果您刚刚粘贴了单个不同的语句,那么编辑引擎会简单地将编辑插入到正确的位置。解析树将重新生成在可能的情况下重新使用现有的绿色节点,并且差异引擎将确定哪些跨度需要重新着色:具有不同绿色节点的跨度。

这一切都有意义吗?

更新:

哈,显然凯文和我都在相邻的办公室同时输入了相同的答案。有点重复的努力,但我认为这两个答案都对这种情况有很好的看法。 :-)

【讨论】:

  • 这更有意义。太感谢了。我一直在为这种操纵找到大量文档时遇到了麻烦,所以这有点像在黑暗中拍摄。
  • @user1059895:不客气。显然,文档还远未完成。也可以在 Roslyn 论坛上随意提问。
  • 不过,据推测,您可以使用 API 遍历两个语法树,以在其结构上执行 semantic 差异。是的?是否有任何内置实用程序或 API 来帮助支持语义差异?
  • @LBushkin:Roslyn 将有一个“推测”语义分析器。也就是说,您可以使用现有程序,然后说“嘿,如果我在程序的这个文本位置插入对 M(x) 的调用,将选择 M 的哪个重载并且会产生任何错误吗?”这是你需要的那种东西吗?
  • @LBushkin:方法是分析区域控制流和分析区域数据流,在 ISemanticModel 接口上找到。它们存在于 CTP 版本中,但我个人不知道我们发布时它们的功能有多全面。这两个都不是我的特色,所以在我们发布之前我没有仔细跟踪他们走了多远。如果您想试一试并向我们发送有关他们是否满足您的需求的反馈,请在论坛上留言并发表您的意见。谢谢!
【解决方案2】:

@bruceboughton 是对的,GetChangedSpans 旨在发现增量解析器所做的更改。使用如下代码,我得到了更好的输出:

        var code = 
        @"using System; 
        using System.Collections.Generic; 
        using System.Linq; 
        using System.Text; 

        namespace HelloWorld 
        { 
            class Program 
            { 
                static void Main(string[] args) 
                { 
                    Console.WriteLine(""Hello, World!""); 
                } 
            } 
        }";
        var text = new StringText(code);
        SyntaxTree tree = SyntaxTree.ParseCompilationUnit(text);

        var index = code.IndexOf("}");
        var added = @"    Console.WriteLine(""jjfjjf""); 
                      ";

        var code2 = code.Substring(0, index) + 
                    added +
                    code.Substring(index);

        var text2 = new StringText(code2);

        var tree2 = tree.WithChange(text2, new [] { new TextChangeRange(new TextSpan(index, 0), added.Length) } );

        foreach (var span in tree2.GetChangedSpans(tree))
        {
            Console.WriteLine(text2.GetText(span));
        }

但是,一般来说 GetChangedSpans 是一种合理的快速但保守的差异。为了更好地控制差异并获得更准确的结果,您可能希望实现自己的树差异算法,您可以对其进行调整以满足您的需求。

在上面的代码中,如果您使用 VS,编辑器内置了更改报告和文本差异,这将允许您轻松构造 TextChangeRange 对象,但否则您可能仍然需要至少一个文本差异如果您希望能够将更改传递给增量解析器,则使用算法。

【讨论】:

    【解决方案3】:

    我猜GetChangedSpans 的目的是比较一棵树和从原始树的更改创建的树之间的变化,而不是两棵任意树之间的变化。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-09-07
      • 1970-01-01
      • 2013-06-13
      • 1970-01-01
      • 2015-03-06
      • 1970-01-01
      • 2011-08-05
      • 1970-01-01
      相关资源
      最近更新 更多