Использование Roslyn для синтаксического анализа/преобразования/генерации кода: я слишком высоко или слишком низко?
(Я пытаюсь решить проблему Application.Settings/MVVM, создав класс интерфейса и оболочки из файла настроек, созданного vs. )
Мне бы хотелось:
- Разберите объявление класса из файла
- Создайте объявление интерфейса, основанное только на (нестационарных) свойствах класса
- Создайте класс-оболочку, который реализует этот интерфейс, берет экземпляр исходного класса в конструкторе и "передает" все свойства в экземпляр.
- Создайте другой класс, который реализует интерфейс напрямую.
Мой вопрос в два раза:
- Я лаяю неправильное дерево? Будет ли я лучше использовать Code-Dom, T4, Regex (!) Для этого или часть этого? (Я не против немного дополнительной работы, поскольку это в основном опыт обучения.)
- Если Рослин - это способ пойти, на какой части я должен смотреть? Я наивно надеялся, что будет какой-то способ ходить по дереву и выплескивать только те куски, которые я хочу, но у меня возникают проблемы с тем, чтобы использовать/использовать SyntaxRewriter для этого или используйте конструкцию в свободном стиле, запрашивая источник несколько раз для необходимых мне битов.
Если вы хотите прокомментировать аспект MVVM, вы можете, но это не главная проблема вопроса:)
Ответы
Ответ 1
Если ваше требование - синтаксический анализ исходного кода на С#, то я думаю, что Roslyn - хороший выбор. И если вы собираетесь использовать его для этой части, я думаю, что имеет смысл использовать ее для генерации кода.
Генерация кода с использованием Roslyn может быть довольно многословной (особенно по сравнению с CodeDom), но я думаю, что это не будет большой проблемой для вас.
Я думаю, что SyntaxRewriter
лучше всего подходит для локализованных изменений кода. Но вы спрашиваете о разборе всего класса и создании типов на основе этого, я думаю, для этого, запрос дерева синтаксиса будет работать лучше всего.
Например, самый простой пример создания интерфейса только для чтения для всех свойств в классе может выглядеть примерно так:
var originalClass =
compilationUnit.DescendantNodes().OfType<ClassDeclarationSyntax>().Single();
string originalClassName = originalClass.Identifier.ValueText;
var properties =
originalClass.DescendantNodes().OfType<PropertyDeclarationSyntax>();
var generatedInterface =
SyntaxFactory.InterfaceDeclaration('I' + originalClassName)
.AddMembers(
properties.Select(
p =>
SyntaxFactory.PropertyDeclaration(p.Type, p.Identifier)
.AddAccessorListAccessors(
SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
.WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken))))
.ToArray());
Ответ 2
Я думаю, что Roslyn - отличный способ решить эту проблему. В терминах какой части Roslyn я бы использовал - я бы, вероятно, использовал SyntaxWalker
над исходным классом, а затем использовал Fluent API для создания нового SyntaxNodes
для новых типов, которые вы хотите сгенерировать. Возможно, вы сможете повторно использовать некоторые части исходного дерева в сгенерированном коде (например, списки аргументов и т.д.).
Быстрый пример того, как это может выглядеть:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Roslyn.Compilers;
using Roslyn.Compilers.CSharp;
using Roslyn.Services;
using Roslyn.Services.CSharp;
class Program
{
static void Main(string[] args)
{
var syntaxTree = SyntaxTree.ParseText(@"
class C
{
internal void M(string s, int i)
{
}
}");
}
}
class Walker : SyntaxWalker
{
private InterfaceDeclarationSyntax @interface = Syntax.InterfaceDeclaration("ISettings");
private ClassDeclarationSyntax wrapperClass = Syntax.ClassDeclaration("SettingsWrapper")
.WithBaseList(Syntax.BaseList(
Syntax.SeparatedList<TypeSyntax>(Syntax.ParseTypeName("ISettings"))));
private ClassDeclarationSyntax @class = Syntax.ClassDeclaration("SettingsClass")
.WithBaseList(Syntax.BaseList(
Syntax.SeparatedList<TypeSyntax>(Syntax.ParseTypeName("ISettings"))));
public override void VisitMethodDeclaration(MethodDeclarationSyntax node)
{
var parameters = node.ParameterList.Parameters.ToArray();
var typeParameters = node.TypeParameterList.Parameters.ToArray();
@interface = @interface.AddMembers(
Syntax.MethodDeclaration(node.ReturnType, node.Identifier.ToString())
.AddParameterListParameters(parameters)
.AddTypeParameterListParameters(typeParameters));
// More code to add members to the classes too.
}
}
Ответ 3
Я делаю что-то очень похожее, и я использую Roslyn для анализа существующего кода С#. Тем не менее, я использую T4 templates для генерации нового кода. Шаблоны T4 предназначены для генерации текста и обеспечивают очень приятную абстракцию, чтобы вы могли фактически указать материал, который СМОТРЕТЬ как код вместо этого сумасшедшего дерева объектов.
Ответ 4
В вопросе генерации кода мой совет состоит в том, чтобы фактически использовать комбинацию встроенных фрагментов кода (проанализировано с использованием CSharpSyntaxTree.ParseText
) и вручную сгенерировано SyntaxNodes
, но с сильным предпочтением первому. Я также использовал T4 в прошлом, но я отхожу от них из-за общего отсутствия интеграции и возможностей.
Преимущества/недостатки каждого:
Roslyn ParseText
- Создает, возможно, более читаемый код-генератор кода.
- Позволяет использовать метод "шаблонирования текста", например. используя интерполяцию строк С# 6.
- Менее подробный.
- Гарантирует действительные деревья синтаксиса.
- Может быть более результативным.
- Легче начать работу.
- Текст может стать более трудным для чтения, чем
SyntaxNodes
, если большинство является процедурным.
Здание Roslyn SyntaxNode
- Лучше для преобразования существующих синтаксических деревьев - не нужно начинать с нуля.
- Но существующие мелочи могут сделать это запутанным/сложным.
- Более подробный. По-видимому, труднее читать и строить.
- Синтаксические деревья часто сложнее, чем вы думаете
-
SyntaxFactory
API предоставляет руководство по допустимому синтаксису.
- Roslyn Quoter помогает преобразовать текстовый код в код factory.
- Синтаксические деревья не обязательно действительны.
- Код, возможно, более прочный после его написания.
Шаблоны T4
- Хорошо, если большинство генерируемых кодов - это плита котла.
- Нет надлежащей поддержки CI.
- Отсутствие подсветки синтаксиса или intellisense без сторонних расширений.
- Индивидуальное сопоставление между входными и выходными файлами.
- Не идеально, если вы делаете более сложное поколение, например. целая иерархия классов, основанная на одном входе.
- По-прежнему возможно использовать Roslyn для "отражения" типов ввода, иначе вы столкнетесь с проблемами с System.Reflection и файловыми замками и т.д.
- Меньший доступный API. T4 включает в себя, параметры и т.д., Может ввести в заблуждение.
Советы по генерации кода Roslyn
- Если вы только разбираете фрагменты кода, например., вам нужно будет использовать
CSharpParseOptions.Default.WithKind(SourceCodeKind.Script)
, чтобы вернуть верные узлы синтаксиса.
- Если вы разборете весь блок кода для тела метода, тогда вам нужно проанализировать его как
GlobalStatementSyntax
, а затем получить доступ к свойству Statement
как BlockSyntax
.
-
Используйте вспомогательный метод для синтаксического анализа одиночного SyntaxNodes
:
private static TSyntax ParseText<TSyntax>(string code, bool asScript = false)
{
var options = asScript
? CSharpParseOptions.Default.WithKind(SourceCodeKind.Script)
: CSharpParseOptions.Default;
var syntaxNodes =
CSharpSyntaxTree.ParseText(code, options)
.GetRoot()
.ChildNodes();
return syntaxNodes.OfType<TSyntax>().First();
}
- При создании
SyntaxNodes
вручную вам обычно нужно сделать окончательный вызов SyntaxTree.NormalizeWhitespace(elasticTrivia: true)
, чтобы сделать код "round-trippable".
- Обычно вы хотите использовать
SyntaxNode.ToFullString()
, чтобы получить фактический текст кода, включая мелочи.
- Используйте
SyntaxTree.WithFilePath()
как удобное место для хранения конечного имени файла, когда вы приходите, чтобы выписать код.
- Если ваша цель состоит в том, чтобы выводить исходные файлы, конечная игра заканчивается действительным
CompilationUnitSyntaxs
.
- Не забывайте красиво печатать, используя
Formatter.Format
как один из заключительных шагов.