Главная страница   /   10.3. Взаимодействие с кодом в среде Visual Studio (Метапрограммирование в .NET

Метапрограммирование в .NET

Метапрограммирование в .NET

Кевин Хазард

10.3. Взаимодействие с кодом в среде Visual Studio

Теперь вы знаете, как генерировать код на лету при помощи Roslyn. Это может иметь огромное положительное влияние на вас как разработчика, который хочет включить методики метапрограммирования в код, потому что вы можете генерировать код на C#, компилировать и выполнить его. Вы используете знания, которые у вас уже есть, вместо того чтобы разбираться в деталях IL. Но вы можете сделать гораздо больше с Roslyn, чем просто компиляция кода. Вы можете использовать его для анализа кода в то время, когда пишется код, и искать альтернативы, вносить исправления. Можно также генерировать код, чтобы упростить реализацию шаблонов и идиом. Давайте взглянем на то, что можно сделать, чтобы исправить WCF проблему, а затем вы увидите, как можно создать код в редакторе.

Создание предупреждения IsOneWay

В разделе 10.1.2 вы видели фрагмент кода, когда были нарушены правила WCF, но C# компилятор посчитал код действительным. Давайте используем Roslyn, чтобы создать пару классов, которые будут интегрированы в редактор Visual Studio, чтобы дать разработчику пользовательскую ошибку и пару обходных путей.

Определение Code Issue

Первое, что вам нужно сделать, это создать проект Code Issue. Это довольно просто, как показано на рисунке 10-8.

Рисунок 10-8: Создание проекта Code Issue. Project Roslyn предоставляет несколько шаблонов проектов, чтобы упростить создание определенного вида решения на основе Roslyn.

Когда вы настроили проект, необходимо создать Code Issue. Это класс, который реализует интерфейс ICodeIssue с парой MEF атрибутов, как показано в следующем листинге.

Листинг 10-5: Создание проекта Code Issue
namespace Wcf.Issues
{
	[ExportSyntaxNodeCodeIssueProvider(
		"Wcf.Issues.OneWayOperationIssueProvider",
		LanguageNames.CSharp,
		typeof(MethodDeclarationSyntax))]
	public sealed class OneWayOperationCodeIssueProvider
		: ICodeIssueProvider
	{

Строки 3-4: Создать MEF экспорт, чтобы определить элементы Code Issue

Строка 5: Определить поддерживаемые языки

Вы утверждаете, что ваш класс является провайдером Code Issue при помощи ExportSyntaxNodeCodeIssueProvider. Тип заявленного узла (MethodDeclarationSyntax) обозначает, что вы ищете объявления методов, которые были созданы или изменены в редакторе. Обратите внимание, что это код для C#. Вы можете легко создать провайдер для VB, но, чтобы сделать это, вам придется использовать версию VB Roslyn.

ICodeIssueProvider определяет три метода GetIssues(). Но вас беспокоит только тот, который принимает CommonSyntaxNode в качестве второго параметра; другие методы вы можете реализовать, выбросив NotImplementedException. Для реализации этого метода, вы должны проверить несколько вещей:

  • Он должен иметь определенный атрибут OperationContractAttribute.
  • IsOneWay должен быть установлен на true.
  • Возвращаемый тип метода должен быть чем-то другим, нежели System.Void.

В следующем листинге показано, как вы можете обработать первые два требования.

Листинг 10-6: Поиск WCF метаданных для метода
var methodNode = (MethodDeclarationSyntax) node;
if (methodNode.Attributes != null)
{
	var model = document.GetSemanticModel();
	var operationContractType =
		typeof (OperationContractAttribute);
	var operationSyntax = (
		from attribute in methodNode.Attributes
		from syntax in attribute.Attributes
		let attributeType = model.GetTypeInfo(syntax).Type
		where
			attributeType != null &&
			attributeType.Name ==
			operationContractType.Name &&
			attributeType.ContainingAssembly.Name ==
			operationContractType.Assembly.GetName().Name
		from argument in syntax.ArgumentList.Arguments
		where (
			argument.NameEquals.Identifier.GetText() ==
				"IsOneWay" &&
			argument.Expression.Kind ==
				SyntaxKind.TrueLiteralExpression)
		select new {syntax, argument}).FirstOrDefault();

Если метод имеет какие-либо атрибуты, вы получаете информацию о типе из семантической модели для этого атрибута и сравниваете его имя и имя сборки со значениями OperationContractAttribute. Посмотрите список аргументов атрибута, и если один из идентификаторов – IsOneWay, а тип выражения – это SyntaxKind.TrueLiteralExpression, вы должны обязательно проверить возвращаемый тип метода.

Третьим требованием является проверка возвращаемого типа метода, как показано в следующем листинге.

Листинг 10-7: Проверка возвращаемого типа метода
if (operationSyntax != null)
	{
		var returnType = model.GetSemanticInfo(
			methodNode.ReturnType).Type;
		if (returnType != null &&
				returnType.SpecialType != SpecialType.System_Void)
		{
			return new[]
			{
				new CodeIssue(CodeIssue.Severity.Error,
					methodNode.ReturnType.Span,
					"One-way WCF operations must return System.Void.",
					new ICodeAction[]
					{
						new OneWayOperationReturnVoidCodeAction(
							editFactory, document,
							methodNode.ReturnType),
						new OneWayOperationMakeIsOneWayFalseCodeAction(
							editFactory, document,
							operationSyntax.argument)
					})
			};
		}
	}
}
return null;

Как только у вас есть семантическая информация о возвращаемом типе метода, вы можете проверить SpecialType чтобы увидеть, равен ли он SpecialType.System_Void. Если это так, то вы должны сказать разработчику, что в коде есть ошибка, и это то, что вы делаете, когда возвращаете объект CodeIssue. В данном случае это определенно ошибка, поэтому используется Severity.Error. Вы хотите, чтобы пользователь сосредоточился на возвращаемом типе в объявлении метода, и именно поэтому вы передаете значение Span свойства ReturnType. Это позволит подчеркнуть красной линией возвращаемый тип в редакторе; скоро вы увидите, как это выглядит.

Другая вещь, которую вы отметите, заключается в том, что вы передаете в объект CodeIssue два объекта через массив ICodeAction. Это дает разработчикам способы решения проблемы, с которой они столкнулись. В следующем разделе вы увидите, как это работает.

Определение действий с кодом OneWayOperation

Одно дело сказать разработчикам, что они делают что-то не так. Но если вы сможете показать им способы решения проблемы, это большая победа. Есть, по крайней мере, два способа решить эту WCF проблему:

  • Изменить значение IsOneWay на false.
  • Изменить возвращаемый тип на System.Void.

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

public sealed class OneWayOperationMakeIsOneWayFalseCodeAction
	: ICodeAction
{
	private IDocument document;
	private AttributeArgumentSyntax attributeArgumentSyntax;
	public OneWayOperationMakeIsOneWayFalseCodeAction(
		IDocument document,
		AttributeArgumentSyntax attributeArgumentSyntax)
	{
		this.document = document;
		this.attributeArgumentSyntax = attributeArgumentSyntax;
	}

Есть три члена в ICodeAction, которые вы должны реализовать. Два свойства, Description и Icon, легко обработать:

public string Description
{
	get { return "Make IsOneWay = false"; }
}
public ImageSource Icon
{
	get { return null; }
}

Главный член, GetEdit(), является методом, в котором вы создадите ваше решение, как показано в следующем листинге.

Листинг 10-8: Сделаем IsOneWay ложным
public CodeActionEdit GetEdit(
	CancellationToken cancellationToken)
{
	var trueToken =
		this.attributeArgumentSyntax.Expression.GetFirstToken();
	var falseToken = Syntax.Token(trueToken.LeadingTrivia,
		SyntaxKind.FalseKeyword, trueToken.TrailingTrivia);
	var tree = (SyntaxTree)this.document.GetSyntaxTree();
	var newRoot = tree.GetRoot().ReplaceToken(trueToken, falseToken);
	return new CodeActionEdit(document.UpdateSyntaxRoot(newRoot));
}}

Это довольно просто. Вам нужно получить ссылку на первый маркер в части Expression атрибута, потому что это то, что вы замените в коде (маркер false). Затем создайте новый маркер через Syntax.Token(), который использует SyntaxKind.FalseKeyword, чтобы определить маркер. Вы сохраняете начальное и конечное форматирование, так что ваш новый маркер легко станет на место старого. Затем вы получаете ссылку на синтаксическое дерево через GetSyntaxTree() для вашего объекта документа. Это используется для замены старого маркера (true) на новый (false) через ReplaceToken(). Последний шаг заключается в возвращении объекта CodeActionEdit, который определяет, как будет выглядеть новый код; в следующем разделе вы увидите, как это работает.

Обратите внимание на мощь неизменности деревьев, которая распространена в Roslyn. Вы можете создать новое дерево на основе старого, которое представляет то, что находится в редакторе кода, но ReplaceToken() не меняет исходное дерево. Если пользователь решит не использовать ваше действие, ничто в его текущем кодовом файле не поменяется.

Другой метод действия меняет тип возвращаемого значения метода на System.Void. Следующий листинг показывает, как выглядит реализация GetEdit() с этим действием.

Листинг 10-9: Реализация GetEdit() для изменения возвращаемого типа на System.Void
public CodeActionEdit GetEdit(
	CancellationToken cancellationToken)
{
	var returnToken = this.typeSyntax.GetFirstToken();
	var voidToken = Syntax.Token(returnToken.LeadingTrivia,
		SyntaxKind.VoidKeyword, returnToken.TrailingTrivia);
	var tree = (SyntaxTree)this.document.GetSyntaxTree();
	var newRoot = tree.GetRoot().ReplaceToken(
		returnToken, voidToken);
	return new CodeActionEdit(document.UpdateSyntaxRoot(newRoot));
}

Листинг 10-9 практически такой же, как листинг 10-8. Главное отличие заключается в том, что вы создаете новый маркер, который использует значение VoidKeyword для SyntaxKind, чтобы изменить тип возвращаемого значения на void.

Когда код на месте, давайте посмотрим, что происходит в Visual Studio.

Просмотр результата

Запустите проект Code Issue в режиме отладки. Это запустит новый экземпляр Visual Studio со включенным Roslyn. Он также включает Code Issue в процесс, так что вы можете отлаживать код в действии. Далее, создайте проект, который ссылается на System.ServiceModel, так чтобы вы смогли использовать OperationContractAttribute. Наконец, добавьте следующий текст в ваш класс:

[OperationContractAttribute(IsOneWay = false)]
public string MyOperation() { return null; }

Последствия изменения возвращаемого типа

Когда вы хотите исправить проблему, исправление может часто ввести другие проблемы. И это тот случай, когда вы меняете тип возвращаемого значения на System.Void. Новая проблема состоит в том, что метод может иметь несколько выражений return, так что теперь вы ввели ряд ошибок, которые разработчик должен исправить.

Есть несколько вещей, которые вы можете сделать. Вы можете изменить тип возвращаемого значения и позволить разработчику обрабатывать любые выражения возврата в коде. Или вы можете изменить действие, чтобы разобрать тело метода и удалить все выражения return. Это может быть не слишком хорошим вариантом, потому что оператор return может содержать побочные эффекты (например, вызов функции, которая меняет состояние). Написание баг-трекингового кода требует изрядного количества анализа, чтобы убедиться, что вы действительно решаете проблему.

Вы заметите, что в окне Error List не отображаются никакие ошибки. Теперь измените IsOneWay на true, и вы увидите новую ошибку в окне Error List, как показано на рисунке 10-9.

Рисунок 10-9: Интеграция пользовательских ошибок в Visual Studio. Вам не нужно ждать окончания компиляции, чтобы статически проанализировать код при помощи Roslyn, вы можете делать это, когда разработчик пишет код.

Помните, когда вы передали значение Span свойства ReturnType? Теперь Visual Studio пометит место в коде, и разработчик сможет исправить ошибку. На рисунке 10-10 показано, как это выглядит в Visual Studio.

Рисунок 10-10: Показ проблемного кода в Visual Studio. Во всплывающем окне вы увидите, что будет, если вы сделаете изменение.

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

Эта мощь и интеграция с Visual Studio возможны благодаря парсинговому движку Roslyn. Вы, наверное, уже думал о других путях расширения опыта в области программирования для решении проблем с кодом. Но вы также можете использовать Roslyn для рефакторинга, так чтобы ваш код можно было легче читать и поддерживать. Давайте посмотрим, как можно провести рефакторинг таким образом, что код будет автоматически следовать конкретному стандарту кодирования.

Автоматическая организация кода

В большинстве проектов по разработке программного обеспечения имеется документация о стандартах кодирования, которая определяет соглашения и идиомы, которым должны следовать все разработчики. Наличие последовательного подхода – это хорошая идея, касательно не только реализации методов, но и стилей форматирования. Но давайте будем честны – отслеживать то, находится ли открывающая фигурная скобка после определения метода или на следующей строке – утомительно. Конечно, хочется, чтобы все использовали одинаковый стиль, но также хочется, чтобы эти правила обрабатывались чем-то еще, например, компьютером. Легкие стили кодирования легко забыть. С Roslyn же вы можете организовать некоторые сложные правила.

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

  • События
  • Поля
  • Конструкторы
  • Методы
  • Свойства
  • Перечисления
  • Вложенные классы или структуры

Давайте посмотрим, что нужно сделать сначала, а потом вы увидите, как это делается в C#.

Определение алгоритма для переформатирования кода

Имеется следующий кусок кода:

public class MyClass
{
	public string Data { get; private set; }
	public void AMethod() { }
	public struct NestedStruct { }
	private int aField;
}

Это не вписывается в формат, указанный в предыдущем разделе. Как вы можете это изменить?

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

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

Например, в MyClass вы найдете четыре члена: свойство (Data), метод (AMethod), вложенный тип (NestedStruct) и поле (aField). Когда вы перечитаете содержимое класса, вы разместите члены в нужном порядке: aField, AMethod, Data и NestedStruct. Затем вы ищете элементы, которые могут быть заменены. Первым вы находите Data. Вы меняете его на aField. Следующий член – это AMethod, который вы замените самим собой: AMethod. Вы заканчиваете последними двумя членами, и получаете измененный порядок определения MyClass:

public class MyClass
{
	private int aField;
	public void AMethod() { }
	public string Data { get; private set; }
	public struct NestedClass { }
}

Как вы увидите в следующих разделах, Roslyn реализует это довольно безболезненно.

Определение основных частей проекта по рефакторингу

Первое, что вы делаете, это создаете проект Code Refactoring из шаблона Roslyn Visual Studio. Как только ваш проект определен, необходимо создать класс, который реализует интерфейс из Roslyn API, и использовать атрибут экспорта MEF, подобно тому, что вы делали с провайдером Code Issue. Следующий фрагмент кода показывает, как это выглядит:

[ExportCodeRefactoringProvider(
	"Core.Refactorings.AutoAlphabetizeCodeRefactoringProvider",
	LanguageNames.CSharp)]
public sealed class AutoArrangeCodeRefactoringProvider
	: ICodeRefactoringProvider
{

Интерфейс ICodeRefactoringProvider определяет один метод, который необходимо реализовать: GetRefactoring().Следующий фрагмент кода показывает, как вы определяете его, чтобы найти определения типов.

Листинг 10-10: Нахождение класса и структурных определений
public CodeRefactoring GetRefactoring(IDocument document,
TextSpan textSpan, CancellationToken cancellationToken)
{
	var token = document.GetSyntaxTree(cancellationToken)
		.Root.FindToken(textSpan.Start);
	var parent = token.Parent;
	if (parent != null && (
		parent.Kind == (int)SyntaxKind.ClassDeclaration ||
		parent.Kind == (int)SyntaxKind.StructDeclaration))
	{
		return new CodeRefactoring(
			new[]
			{
			new AutoArrangeCodeAction(
				this.editFactory, document,
				parent as TypeDeclarationSyntax)
			});
	}
	return null;
}

Помните, что вам нужно переформатировать классы или структуры, чтобы они соответствовали определенному стилю, так что вы должны убедиться, что текст, который пользователь выделил в Visual Studio, является частью объявления типа. Это то, что вы делаете, когда исследуете узел Parent в маркере, который отображает предоставленный TextSpan (выделенный текст в IDE). Как только вы определили, что родительский Kind – это либо ClassDeclaration, либо StructDeclaration, вы передаете родительский узел как объект TypeDeclarationSyntax объекту AutoArrangeCodeAction (вместе с объектами editFactory и document). Этот объект действия кода делает тяжелую работу, чтобы представить ваш класс в нужном формате.

Создание действия кода

В разделе 10.3.3 вы видели, как создать класс на основе ICodeAction, поэтому давайте сосредоточимся на реализации GetEdit():

public CodeActionEdit GetEdit(
	CancellationToken cancellationToken)
{
	var captureWalker = new AutoArrangeCaptureWalker();
	captureWalker.VisitTypeDeclaration(this.token);
	var result = new AutoArrangeReplaceRewriter(
		captureWalker).VisitTypeDeclaration(this.token);
	var tree = (SyntaxNode)this.document.GetSyntaxRoot(
		cancellationToken);
	var newTree = tree.ReplaceNodes(new [] { this.token },
		(a, b) => result);
	return new CodeActionEdit(document.UpdateSyntaxRoot(newTree));
}

Можно, наверное, догадаться, что делают классы AutoArrangeCaptureWalker и AutoArrangeReplaceWriter, основываясь на их именах. Первый находит все члены, которые вы хотите переместить, а второй выполняет перемещение. Давайте посмотрим, как работает класс-визитер, рассмотрев VisitTypeDeclaration():

public void VisitTypeDeclaration(TypeDeclarationSyntax node)
{
	this.Target = node;
	var classNode = node as ClassDeclarationSyntax;
	if (classNode != null)
	{
		base.VisitClassDeclaration(classNode);
	}
	else
	{
		base.VisitStructDeclaration(
			node as StructDeclarationSyntax);
	}
}

Вы хотите посетить все элементы в целевом типовом узле. Таким образом, вы говорите базовой реализации посетить класс или структуру, в зависимости от того, каким является тип узла. SyntaxWalker затем посещает все дочерние узлы внутри определения типа, что означает, что вам необходимо переопределить несколько методов VisitXYZDeclaration(), чтобы словить элементы. Вот как выглядит VisitEnumDeclaration() (другие переопределенные варианты элементов практически одинаковы):

protected override void VisitEnumDeclaration(
	EnumDeclarationSyntax node)
{
	this.Enums.Add(node);
}

Списки инициализируются во время создания для хранения узлов, которые вам интересны. Вы вставляете узел в этот список, когда этот узел посещается.

Единственным исключением из этого правила являются вложенные типы. Вот как вы обрабатываете эти узлы:

protected override void VisitClassDeclaration(
	ClassDeclarationSyntax node)
{
	var capture = new AutoArrangeCaptureWalker();
	capture.VisitTypeDeclaration(node);
	this.Types.Add(capture);
}

Вы создаете новый объект AutoArrangeCaptureWalker для этого вложенного типа и позволяете найти всех членов вложенного типа. Затем вы сохраняете этого визитера в списке, который вы будете использовать, когда вы реорганизуете определение корневого типа. Обратите внимание, что метод VisitStructDeclaration() работает точно так же, как VisitClassDeclaration().

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

Листинг 10-11: Пересортировка всех членов типа
public AutoArrangeReplaceRewriter(
	AutoArrangeCaptureWalker rewriter)
{
	rewriter.Constructors.Sort(
		(a, b) => a.Identifier.ValueText.CompareTo(
			b.Identifier.ValueText));
	rewriter.Enums.Sort(
		(a, b) => a.Identifier.ValueText.CompareTo(
			b.Identifier.ValueText));
	rewriter.Events.Sort(
		(a, b) => a.Identifier.ValueText.CompareTo(
			b.Identifier.ValueText));
	rewriter.Fields.Sort(
		(a, b) => a.Declaration.Variables[0]
			.Identifier.ValueText.CompareTo(
				b.Declaration.Variables[0]
					.Identifier.ValueText));
	rewriter.Methods.Sort(
		(a, b) => a.Identifier.ValueText.CompareTo(
			b.Identifier.ValueText));
	rewriter.Properties.Sort(
		(a, b) => a.Identifier.ValueText.CompareTo(
			b.Identifier.ValueText));
	rewriter.Types.Sort(
		(a, b) => a.Target.Identifier.ValueText.CompareTo(
			b.Target.Identifier.ValueText));
	this.nodes = new List<SyntaxNode>();
	this.nodes.AddRange(rewriter.Events);
	this.nodes.AddRange(rewriter.Fields);
	this.nodes.AddRange(rewriter.Constructors);
	this.nodes.AddRange(rewriter.Methods);
	this.nodes.AddRange(rewriter.Properties);
	this.nodes.AddRange(rewriter.Enums);
	this.nodes.AddRange(
		from typeRewriter in rewriter.Types
		select new AutoArrangeReplaceRewriter(typeRewriter)
			.VisitTypeDeclaration(typeRewriter.Target)
				as TypeDeclarationSyntax);
}

Каждый член рассортирован в свой список, и создается новый список, который содержит пересортированные члены в правильном порядке. Вложенные типы обрабатываются путем создания AutoArrangeReplaceRewriter для этого типа, а результаты VisitTypeDeclaration() вставляются в список узлов.

Как и в случае с AutoArrangeCaptureRewriter, пользовательский метод VisitTypeDeclaration() создается для того, чтобы посетить (и впоследствии заменить) все члены членами в правильной последовательности:

public TypeDeclarationSyntax VisitTypeDeclaration(
	TypeDeclarationSyntax node)
{
	var classNode = node as ClassDeclarationSyntax;
	if (classNode != null)
	{
		return base.VisitClassDeclaration(classNode)
		as ClassDeclarationSyntax;
	}
	else
	{
		return base.VisitStructDeclaration(
		node as StructDeclarationSyntax)
		as StructDeclarationSyntax;
	}
}

Аналогичным образом переопределяются желаемые методы VisitXYZDeclaration(), чтобы обработать стратегию замены. Вы можете увидеть, как это работает, на примере с VisitEnumDeclaration():

protected override SyntaxNode VisitEnumDeclaration(
	EnumDeclarationSyntax node)
{
	return this.Replace(node);
}

Метод Replace() обрабатывает изменение порядка:

private SyntaxNode Replace(SyntaxNode node)
{
	SyntaxNode result = null;
	if (this.count < this.nodes.Count)
	{
		result = this.nodes[this.count];
		this.count++;
	}
	else
	{
		throw new NotSupportedException();
	}
	return result;
}

На данный момент все просто. Вы должны отслеживать, как много членов вы уже посетили (что делается при помощи поля) и заменить текущий на правильный в списке узлов.

Теперь давайте посмотрим, как это выглядит, когда вы используете это в Visual Studio.

Просмотр результатов

Как только ваш проект запускает экземпляр Visual Studio на основе Roslyn, вы можете создать новый проект с классом, у которого члены располагаются в любом порядке. Затем выберите текст sealed для определения данного класса – вы должны увидеть небольшой сероватый прямоугольник слева от выделенного текста. Если вы нажмете Ctrl +., то вы увидите окно предварительного просмотра результатов, как показано на рисунке 10-11.

Рисунок 10-11: Предварительный просмотр рефакторинга автоматического упорядочивания. Как вы можете видеть, положение членов уже изменилось – и пользователь решает, принимать ли изменения.

Если вы согласны с изменениями, члены будут перемещены в IDE, и вы получите результат, который выглядит примерно как на рисунке 10-12.

Рисунок 10-12: Автоорганизация кода. как только рефакторинг сделан, члены находятся в правильном порядке.

С Roslyn вы можете делать почти все, что хотите, с вашим кодом в редакторе. Самое главное - это выяснить, как работать с API для получения желаемого результата.

Что насчет CodeDOM?

Как вы видели в главе 4, в .NET уже есть API, называемый CodeDOM, который может показаться похожим на Roslyn на первый взгляд. Но CodeDOM более ограничен, чем Roslyn. Полное описание различий между этими API можно увидеть на http://mng.bz/Tx53.