Разница в коде с использованием Roslyn CTP API - PullRequest
12 голосов
/ 29 ноября 2011

Я пытаюсь выполнить базовый анализ кода с помощью 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 . . .

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

Ответы [ 3 ]

17 голосов
/ 29 ноября 2011

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

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

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

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

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

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

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

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

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

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

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

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

UPDATE:

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

12 голосов
/ 29 ноября 2011

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

6 голосов
/ 29 ноября 2011

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

...