Pull to refresh

Пишем расширения c Roslyn к 2015 студии (часть 2)

.NET *Visual Studio *C# *
Tutorial
… Эта статья является продолжением первой части о написании расширений к студии с Roslyn.

Тут я буду описывать что делать, если мы хотим сгенерировать/поменять какой-нибудь код. Для генерации кода мы будем статические методы класса SyntaxFactory. Некоторые методы требуют указать ключевое слово/тип выражения/тип токена, для этого есть перечисление — SyntaxKind, который содержит все это вместе.

Хорошо, давайте для примера сгенерируем код, содержащий число 10. Это делается просто.

SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(10))

Я не шутил, когда говорил, что чтобы создать код проще всего распарсить строку. Благо, SyntaxFactory предоставляет кучу методов для этого (ParseSyntaxTree, ParseToken, ParseName, ParseTypeName, ParseExpression, ParseStatement, ParseCompilationUnit, Parse*List).

Но это не путь настоящего самурая.

Итак, SyntaxFactory


Ок, давайте мы избавимся от первой же ошибки, которую я совершил. Я забыл что C# теперь 6-й версии. А одной из фишек C# 6 является статический импорт. Давайте захламим нашу глобальную область видимости.

using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Microsoft.CodeAnalysis.SymbolKind;
...
LiteralExpression(NumericLiteralExpression, Literal(10))

Чуточку лучше, но все равно выглядит не очень. Если мы такое будем писать в коде, то очень быстро потеряем контекст и забудем что мы хотели сделать. И погрязнем в деталях реализации. Это — нечитабельный код.

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

Например, так вот:

public static LiteralExpressionSyntax ToLiteral(this int number) {
	return LiteralExpression(NumericLiteralExpression, Literal(number));
}

10.ToLiteral()


Уже чуточку лучше. Вам может не понравиться, что мы захламляем для всех intов область видимости нашими методами. Но для меня нормально писать DSL.

Хорошо, давайте попробуем вызывать метод. Для этого есть метод InvocationExpression, первым параметром которого идет выражение, описывающее метод (например this.Invoke, или my.LittlePony.Invoke()), вторым параметром идет список аргументов методу.

Т.е. если мы хотим вызывать метод this.Add(1, 2), то нам нужно написать нечто такое:


var method = MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, ThisExpression(), IdentifierName("Add")) // this.Add
var argument1 = Argument(1.ToLiteral());
var argument2 = Argument(2.ToLiteral());
var arguments = ArgumentList(SeparatedList(new [] { argument1, argument2 }));
var invocation = InvocationExpression(method, arguments)


Никакой красоты кода. Давайте напишем пару DSL-методов (это именно DSL, а не хелперы. Они не приносят в код ничего, ни возможности переиспользования кода, ни группировку кода по смыслу).

Во-первых, 1-ю строку можно записать так:
var method = This().Member("Add");
А последние четыре можно записать например так:
InvocationExpression(method, 1.ToLiteral(), 2.ToLiteral())
Дальше, сокращаем до одной строки:
This().Member("Add").ToInvocation(1.ToLiteral(), 2.ToLiteral())
Или так:
This().ToInvocation("Add", 1. ToLiteral(), 2.ToLiteral())


Тут все просто, я не буду описывать какие методы расширения я написал — они тупы и очевидны.

Уже на порядок лучше, но все равно не красиво. Вот бы избавиться от этого унылого ToLiteral(). Только ведь у нас аргументы это как минимум ExpressionSyntax, никуда мы от этого не денемся. Если мы заставим метод ToInvocation принимать только числа, то не сможем туда передавать что-нибудь другое. Или денемся?

Давайте введем тип AutoExpressionWrapper.

public struct AutoExpressionWrapper {
	public ExpressionSyntax Value { get; }
	
	public AutoExpressionWrapper(ExpressionSyntax value) {
		Value = value;
	}
		
	public static implicit operator AutoExpressionWrapper(int value) {
		return new AutoExpressionWrapper(value.ToLiteral())
	}

	public static implicit operator AutoExpressionWrapper(ExpressionSyntax value) {
		return new AutoExpressionWrapper(value);
	}
}
	
public static InvocationExpressionSyntax ToInvocation(this string member, params AutoExpressionWrapper[] expressions) {
        return InvocationExpression(IdentifierName(member), expressions.ToArgumentList());
}
	
This().ToInvocation("Add", 1, 2);


Красота.

Правда достигается это таким количеством левого кода, что мама не горюй. Ну и ладно. Зато красиво и гораздо более понятно.

Тут у каждого свой выбор, можно не писать такую кучу кода, а останавливаться пораньше — где душа пожелает. Лично у меня процесс разработки выглядит так:
  • Написал пример кода, который хочу сгенерировать
  • С помощью Roslyn Syntax Visualizer посмотрел во что он парсится
  • Нашел соответствующие методы в SyntaxFactory, написал код, проверил, что он работает правильно
  • Переписал все, чтобы было красиво и пока не лень
  • Через пару дней вернулся, понял что код плох, переписал
  • Узнал о неизвестном ранее мне API, расстроился


Давайте сделаем что-нибудь простое, например мы хотим сгенерировать код типа "!condition". Здесь у нас есть идентификатор и логическое отрицание. В коде это выглядит так:

PrefixUnaryExpression(LogicalNotExpression, IdentifierName("condition"))

С легкого движения руки получается так:

LogicalNot(IdentifierName("condition"))

У вас могут случиться проблемы с пониманием, какой SyntaxKind в каком методе SyntaxFactory можно использовать. Вам в этом может помочь анализ SyntaxKindFacts.cs

Аналогично генерируется к примеру, «a != b»:

BinaryExpression(NotEqualsExpression, left, right)

Ок, давайте сделаем что-то посложнее — объявим целую переменную! Для этого нам нужно всего лишь создать VariableDeclaration. Но поскольку в С# запись int a = 2, b = 3; является верной корректной записью, то VariableDeclaration — это тип переменных + список переменных. Сама переменная (a = 2) к примеру — это VariableDeclarator. А что такое инициализатор? Просто выражение представляющее число «2»? Нетушки, это выражение представляющее " = 2". И если мы хотим объявить переменную «int a = 2;», то у нас будет такой код:

VariableDeclaration(
     IdentifierName("int"), 
     SeparatedList(new [] {
          VariableDeclarator("a").WithInitializer(EqualsValueClause(2.ToLiteral())) 
     }))

Ладно, а если мы хотим объявить protected поле? Ну мы должны сделать так:
FieldDeclaration(variableDeclaration).WithModifiers(TokenList(new [] { Token(ProtectedKeyword) }))


Самый большой прикол в том, что сущности поля, свойства, события, метода, конструктора имеют модификаторы доступа. Но на коде это никак не отображено. У каждого класса, который представляет какую-то сущность просто определенны методы для работы с модификаторами. Т.е. вы не можете написать общий метод, который делает все красиво (разве что через dynamic).

А теперь присвоим переменной какое-нибудь значение. Для этого нужно объявить выражение (Expression) присваивания (Assigment) и завернуть его в во-что нибудь более самостоятельное — ExpressionStatement или ReturnStatement.
ExpressionStatement(AssignmentExpression(SimpleAssignmentExpression, IdentifierName("a"), 10.ToLiteral())))


А уж если вы хотите определить метод, в котором выполняется подобное присваивание, то сначала бы неплохо объединить кучу Statements в один с помощью BlockSyntax. Кстати, метод определяется на удивление просто

SyntaxFactory
	.MethodDeclaration(returnType: returnType, identifier: "DoAssignment")
	.WithParameterList(SyntaxFactory.ParameterList()) // заменить со своим списком параметров
	.WithBody(codeBlock) // codeBlock - это BlockSyntax 

Еще можно указать модификаторы доступа, если вам все еще хочется.

SyntaxGenerator


Но, к вашему счастью не все так плохо. SyntaxFactory — это низкоуровненное API для генерации кодоузлов. Про него надо знать. Но множество вещей можно сделать с помощью SyntaxGenerator, и ваш код будет чище и красивее. Единственный его недостаток в том, что он не статический класс. Это может помешать разработке своих DSL, но SyntaxGenerator — это явное движение по читабельности кода вперед.

Получить его можно так:

SyntaxGenerator.GetGenerator(document)

А теперь можно попробовать сгенерировать поле.

generator.FieldDeclaration(
	"_myField", // имя поля
	IdentifierName("int"), // здесь указываем тип
	Accessibility.ProtectedOrInternal, // будет преобразовано в protected internal для С#
	DeclarationModifiers.ReadOnly, 
	2.ToLiteral() // да, не нужно оборачивать EqualsValueClause
)

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

Также в SyntaxGenerator вы можете найти всякие методы типа WithStatements, которые предназначены для получения информации или создания откорректированного кодоузла. Они тоже чуточку более высокоуровневые, чем методы, которые определенны в SyntaxNode и производных.

DocumentEditor


Вы же помните, что все кодоузлы — это персистентные неизменяемые деревья? Итак, после создания какого-нибудь кодоузла его нужно привести в порядок, добавить недостающих моментов, а потом вставить в какой-нибудь другой кодоузел. А потом заменить в корне старый кодоузел, на измененный. А если их много? Это неудобно.

Но есть такая клевая штука DocumentEditor — она превращает работу с неизменяемыми деревьями в последовательность итеративных действий. Создается так:

DocumentEditor.CreateAsync(document, token)

Ну или можно создавать SyntaxEditor (наследником которого и является DocumentEditor).

В SyntaxEditor определенны методы для замены узла, добавления, удаления и получения измененного дерева. Также есть куча полезных методов расширений в SyntaxEditorExtensions. Потом измененное дерево можно получить с GetChangedRoot, а измененный документ с GetChangedDocument. Подобный функционал но в размерах солюшена организован в виде SolutionEditor.

Увы, но высокоуровненный API пока что еще не полностью протестирован и есть кое-какие баги.

Приятной кодогенерации.
Tags:
Hubs:
Total votes 27: ↑27 and ↓0 +27
Views 6.6K
Comments Comments 2