Код diff с использованием API-интерфейса Roslyn CTP

Я пытаюсь выполнить базовый код с API Roslyn, и у меня возникают непредвиденные проблемы. По сути, у меня есть два кода, которые одинаковы, кроме одной строки. Это должно просто вернуть строку измененного текста, но по какой-то причине это говорит мне, что все изменилось. Я также попытался просто отредактировать одну строку вместо добавления строки, но я получаю тот же результат. Я хотел бы иметь возможность применить это к двум версиям исходного файла, чтобы идентифицировать различия между ними. Здесь код, который я использую в настоящее время:

        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 . . .

Интересно, что каждая строка отображается как токены для каждой строки, за исключением добавленной строки, где отображается строка, не разбирая ее. Кто-нибудь знает, как изолировать фактические изменения?

Ответ 1

Уверенность Брюса Боутона верна. Метод GetChangedSpans не должен быть механизмом различения синтаксиса общего назначения, чтобы разделить два дерева синтаксиса, которые не имеют общей истории. Скорее, предполагается, что два дерева, которые были созданы путем редактирования в общее дерево, и определяют, какие части деревьев отличаются из-за изменений.

Если вы взяли свое первое дерево синтаксического анализа и ввели в него новое утверждение как редактирование, тогда вы увидите гораздо меньший набор изменений.

Это может помочь, если я кратко опишу, как работает лексир и парсер Roslyn на высоком уровне.

Основная идея заключается в том, что лексерские "синтаксические токены" и синтаксические деревья, созданные парсером, неизменяемы. Они никогда не меняются. Поскольку они никогда не меняются, мы можем повторно использовать части предыдущих деревьев синтаксического анализа в новых деревьях синтаксического анализа. (Структуры данных, которые имеют это свойство, часто называют "постоянными" структурами данных.)

Поскольку мы можем повторно использовать существующие части, мы можем, например, использовать одно и то же значение для каждого экземпляра данного токена, например class, который появляется в программе. Длина и содержание каждого токена class точно совпадают; единственное, что отличает два разных токена class, - это их мелочи (что окружает их промежутки и комментарии), а также их положение и их родительский элемент - какой более синтаксис node содержит токен.

При анализе блока текста мы генерируем синтаксические токены и деревья синтаксиса в постоянной, неизменной форме, которую мы называем "зеленой" формой. Затем мы завершаем зеленые узлы в "красном" слое. Зеленый слой ничего не знает о позиции, родителях и т.д. Красный слой. (Причудливые имена связаны с тем, что, когда мы впервые нарисовали эту структуру данных на доске, это те цвета, которые мы использовали.) Когда вы создаете редактирование для данного дерева синтаксиса, мы смотрим на предыдущее дерево синтаксиса, идентифицируем узлы, которые изменились, а затем построили новые узлы только по позвоночнику изменений. Все остальные ветки зеленого дерева остаются неизменными.

При различении двух деревьев, в основном, мы делаем разницу между зелеными узлами. Если одно из деревьев было создано путем редактирования другого, то почти все зеленые узлы будут одинаковыми, потому что только спинной хребет был перестроен. Алгоритм древовидной идентификации идентифицирует измененные узлы и обрабатывает затронутые промежутки.

Если два дерева не имеют общей истории, единственными зелеными узлами, которые у них будут общего, являются индивидуальные жетоны, которые, как я уже говорил, используются повсеместно. Каждый зеленый синтаксис более высокого уровня node будет отличаться от зеленого node и поэтому будет отличаться от механизма разности деревьев, даже если его текст одинаков.

Цель этого метода состоит в том, чтобы позволить редактору быстро сделать консервативное предположение о том, какие части текстового буфера должны быть, скажем, перекрашены, после редактирования или отмены, или какой-то такой вещи. Предполагается, что деревья имеют исторические отношения. Цель состоит не в том, чтобы обеспечить механизм текстовых различий общего назначения; для этого есть много отличных инструментов.

Представьте, например, что вы ввели свою первую программу в редактор, а затем выделили все, а затем вставили вторую программу в редактор. Можно было бы разумно ожидать, что редактор не будет тратить время на попытки выяснить, какие части вставного кода оказались идентичными с ранее вставленным кодом. Это может быть очень дорого, и ответ, скорее всего, будет "немного". Скорее, редактор делает консервативное предположение, что весь вложенный регион является совершенно новым и совершенно другим кодом. Он не тратит время, пытаясь сделать соответствия между старым кодом и новым кодом; он репарации и, следовательно, перекрашивает все это.

Если, с другой стороны, вы только что вставили в одном отдельном выражении, то механизм редактирования просто введет редактирование в нужное место. Дерево синтаксического анализа было бы регенерировано повторно используя существующие зеленые узлы, где это возможно, и механизм различий определит, какие промежутки должны быть перекрашены: те, у которых разные зеленые узлы.

Все это имеет смысл?

UPDATE:

Ха, по-видимому, мы с Кевином одновременно печатали один и тот же ответ в соседних офисах. Немного дублированных усилий, но я думаю, что оба ответа имеют хорошие перспективы для ситуации.: -)

Ответ 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 должен быть разумным быстрым, но консервативным diff. Для большего контроля над diff и более точных результатов вы, вероятно, захотите реализовать свой собственный алгоритм, который можно настроить, чтобы удовлетворить ваши потребности.

В приведенном выше коде, если вы используете VS, редактор имеет встроенные отчеты об изменениях и текстовые отличия, которые позволят вам легко создавать объекты TextChangeRange, но в противном случае вам, вероятно, по-прежнему понадобится хотя бы текст, отличный от алгоритм, если вы хотите иметь возможность передавать изменения в инкрементный синтаксический анализатор.

Ответ 3

Я бы предположил, что GetChangedSpans предназначен для сравнения изменений между деревом и деревьями, созданными из изменений исходного дерева, а не между двумя произвольными деревьями.