使用Roslyn CTP API的代码差异

pie*_*bie 12 .net c# roslyn

我正在尝试使用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));
        }
Run Code Online (Sandbox Code Playgroud)

这是我得到的输出:

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 . . .
Run Code Online (Sandbox Code Playgroud)

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

Eri*_*ert 17

Bruce Boughton的猜测是正确的.GetChangedSpans方法不是一种通用语法差异机制,用于区分两个没有共享历史记录的语法树.相反,它旨在将通过编辑生成的两棵树带到一棵公共树,并确定由于编辑而树的哪些部分是不同的.

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

如果我在高层次上简要描述Roslyn词法分析器和解析器的工作方式,可能会有所帮助.

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

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

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

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

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

此方法的目的是允许编辑器代码快速做出保守猜测,即文本缓冲区的哪些部分需要,例如,重新编辑,编辑后,撤消或某些此类事物.假设树木具有历史关系.目的不是提供通用的文本差异机制; 已经有很多很棒的工具.

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

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

这一切都有意义吗?

更新:

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

  • @LBushkin:Roslyn将拥有一个"推测性"语义分析器.也就是说,你可以拿一个现有的程序,然后说"嘿,如果我在程序的这个文本位置插入一个调用`M(x)`,那么M​​的重载会被选中,是否会产生错误? " 这是你需要的那种吗? (2认同)
  • @LBushkin:方法是在ISemanticModel接口上找到的AnalyzeRegionControlFlow和AnalyzeRegionDataFlow.它们存在于CTP版本中,但我个人不知道它们在发布时的功能是多么全面.这些都不是我的功能所以我没有仔细跟踪他们在我们发布之前走了多远.如果您想给他们一个旋转并向我们发送关于他们是否满足您的需求的反馈,请在论坛上留言并留下您的意见.谢谢! (2认同)

Kev*_*lch 12

@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));
        }
Run Code Online (Sandbox Code Playgroud)

但是,一般来说,GetChangedSpans意味着快速而又保守的差异.为了更好地控制差异,以及更准确的结果,您可能希望实现自己的树差异算法,您可以调整以满足您的需求.

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


bru*_*ton 6

我猜这GetChangedSpans是为了比较树和树之间的变化,这些变化是根据原始树的变化而不是两棵任意树之间的变化.