Презентация к докладу «Roslyn API: SyntaxTree vs CodeDom, SemanticModel vs Reflection» с конференции .NEXT SPb 2015 (Санкт-Петербург, 5 июня 2015)
http://spb2015.dotnext.ru/#cvetkih_talk
Roslyn API: SyntaxTree vs CodeDom, SemanticModel vs Reflection
1. Roslyn API
SyntaxTree vs CodeDom
SemanticModel vs Reflection
Денис Цветцих
АстроСофт
www.astrosoft.ru
2. Почему это важно
О Roslyn много говорят
• Революция в мире компиляторов
• Позволит развивать C# быстрее
• OpenSource на GitHub
• Nuget packages Microsoft.CodeAnalysis.*
Что он даст для решения прикладных задач?
• Анализ кода
• Кодогенерация
2
3. Почему я здесь
Решал задачу кодогенерации
• Roslyn: Syntax tree
• CodeDom
Решал задачу анализа кода
• Roslyn: Semantic model
• Reflection
3
Cравнение Roslyn с
CodeDom и Reflection
4. Опрос
Кто решал задачи:
• кодогенерации с помощью CodeDom?
• анализа кода с помощью Reflection?
Кто сделал хотя бы один сэмпл использования Roslyn?
Кто применял Roslyn в своих проектах?
4
5. План на сегодня
• Возможности Roslyn для кодогенерации и анализа кода
• Какие из них и почему мы использовали
• Сравнение Roslyn с CodeDom и Reflection
5
6. Какая стояла задача
Клиент для ONVIF камеры на Windows Phone 8.1
• Работа с камерой через SOAP
• Но Windows Phone 8.1 не поддерживает SOAP сервисы
6
7. Решение: аналог “Add Service Reference”
• Собственный класс SoapClientBase
• По WSDL генерируем код утилитой SvcUtil
• Анализируем код при помощи Reflection
• Генерируем клиент SOAP сервиса при помощи CodeDom
• Анализ кода – SemanticModel
• Генерация кода – SyntaxTree
7
8. Абстрактное синтактическое дерево
Абстрактное синтактическое дерево (АСД,
англ. Abstract Syntax Tree, AST) – древовидное представление
синтаксической структуры программы
Узлы – конструкции языка программирования (классы,
методы, операторы)
Листья – неделимые синтаксические конструкции
(переменные, константы, ключевые слова)
8
11. Решение задачи №1
• Строим AST для кода, сгенерированного SvcUtil для .NET
• Перестраиваем с учетом специфики Windows Phone 8.1
• Сохраняем код перестроенного дерева
11
15. Решение задачи №2
• Строим AST для кода, сгенерированного SvcUtil для .NET
• Анализируем AST. Получаем:
• интерфейсы, помеченные [ServiceContract]
• классы, помеченные [DataContract]
• На основе анализа строим AST для Windows Phone 8.1
• Сохраняем код построенного дерева
15
16. CSharpSyntaxWalker
Тот же самый Visitor
Перегрузка методов void Visit*
void VisitClassDeclaration(ClassDeclarationSyntax node)
void VisitInterfaceDeclaration(InterfaceDeclarationSyntax node)
16
17. Использование SyntaxWalker
1 private readonly SemanticModel _semanticModel;
2 public readonly List<INamedTypeSymbol> Services;
3
4 override void VisitInterfaceDeclaration(InterfaceDeclarationSyntax node)
5 {
6 var symbol = _semanticModel.GetDeclaredSymbol(node);
7 if (symbol.GetAttributes()
8 .Any(attr => attr.AttributeClass.Name == "ServiceContractAttribute"))
9 Services.Add(symbol);
10 }
17
18. API для кодогенерации
• Наследники SyntaxNode имеют internal конструктор
• Создание экземпляров при помощи SyntaxFactory
• Редактирование при помощи Fluent API методов With*
var decl = SyntaxFactory.ClassDeclaration("MyClass")
.WithBaseList(baseList)
.WithModifiers(modifiers)
.WithAttributeLists(attributes);
18
19. Создание namespace
Как реализовано
NameSyntax nameSyntax =
SyntaxFactory.ParseName("MyNamespace");
NamespaceDeclarationSyntax ns =
SyntaxFactory.NamespaceDeclaration(nameSyntax);
Как хочется
NamespaceDeclarationSyntax ns =
SyntaxFactory.NamespaceDeclaration("MyNamespace");
19
20. Добавление директивы using
Как реализовано
NamespaceDeclarationSyntax ns = …
var name = SyntaxFactory.ParseName("System");
var usingSyntax = SyntaxFactory.UsingDirective(name);
ns = ns.AddUsings(usingSyntax);
Как хочется
ns = ns.AddUsings("System");
20
21. Создание поля класса
Как реализовано
TypeSyntax type = ...
var variable = SyntaxFactory.VariableDeclarator("x");
var variableDecl = SyntaxFactory.VariableDeclaration(type);
var variableList = SyntaxFactory.SeparatedList(new[] {variable});
variableDecl = variableDecl.WithVariables(variableList);
var field = SyntaxFactory.FieldDeclaration(variableDecl);
// private int x, y, z;
// private int x;
Как хочется
var field = SyntaxFactory.FieldDeclaration("x", type);
21
22. Можно добавить много, нельзя один
• Переменная
• Модификатор класса, метода, свойства, …
• Реализуемые интерфейсы
22
23. SyntaxTree.Parse*
Преобразование строки в SyntaxNode
var body = string.Format(
"return this.CallAsync<{0}, {1}>("{2}", {3}.{4});",
v1, v2, v3, v4, v5);
SyntaxNode stmt = SyntaxFactory.ParseStatement(body);
23
24. Workspace
• MSBuildWorkspace – рабочая область
• Solution – набор проектов
• Project – проект, содержащий файлы исходного кода
• Document – файл исходного кода, содержит SyntaxTree и
соответствующую SemanticModel
24
26. Syntax API для кодогенерации
• Предназначено для решения общих задач
• Добавить классу список модификаторов
• API для решения частных задач отсутствует
• Нельзя добавить классу один модификатор
• Многословно
• Создать название, а потом уже namespace
• Решение – extension методы
26
28. SyntaxTree vs CodeDom
SyntaxTree
± Нет API для решения частных
задач (extension method)
± Сразу генерируется код на C#
+ Наличие методов Parse*
CodeDom
+ Есть API для решения частных
задач
+ Строится модель, по ней
можно сгенерить C# и VB
28
29. Анализ кода
Compilation – аналог проекта. Содержит:
• Список элементов для компиляции
• Assembly references
29
30. Создание CSharpCompilation
SyntaxTree syntaxTree =
CSharpSyntaxTree.ParseText(File.ReadAllText(csFilePath));
var mscorlib =
MetadataReference.CreateFromAssembly(typeof(object).Assembly);
var compilation = CSharpCompilation.Create("ServiceReference",
new[] { syntaxTree },
new[] { mscorlib });
var semanticModel = compilation.GetSemanticModel(syntaxTree);
30
31. SemanticModel
Extension метод GetDeclaredSymbol:
по узлу SyntaxTree получить соответствующий семантический объект
IPropertySymbol GetDeclaredSymbol(PropertyDeclarationSyntax syntax)
INamedTypeSymbol GetDeclaredSymbol(BaseTypeDeclarationSyntax syntax)
31
32. Получение свойств класса
Как реализовано
ITypeSymbol typeSymbol = …
var properties = typeSymbol
.GetMembers()
.Where(m => m.Kind == SymbolKind.Property)
.Cast<IPropertySymbol>();
Как хочется
var properties = typeSymbol.GetProperties();
32
33. Является ли тип примитивом
Стандартной реализации нет. Можно реализовать так:
ITypeSymbol type;
var primitives = new HashSet<string>
{
"byte", "int", ext.
};
bool isPrimitive = primitives.Contains(type.ToString());
33
37. Ищем Type в NamedArguments
1 AttributeData attribute = … // анализируемый атрибут
2 TypedConstant? dataType; // результат
3
4 var named = attr.NamedArguments
5 .FirstOrDefault(item => item.Key == "Type");
6 if (!named.Equals(default(KeyValuePair<string, TypedConstant>)))
7 dataType = named.Value;
8 else // будем искать в параметрах конструктора
9 Продолжение на следующем слайде
37
38. Ищем Type в ConstructorArguments
9 if (attribute.ConstructorArguments.Length == 2)
10 dataType = attribute.ConstructorArguments.Last();
11 else
12 {
13 var arg = attribute.ConstructorArguments.FirstOrDefault();
14 if (!arg.Equals(default(TypedConstant)) &&
15 arg.Type.Name == "Type") //"Type" – System.Type
16 dataType = arg;
17 }
38
39. Собираем все вместе
1 AttributeData attribute = … // анализируемый атрибут
2 TypedConstant? dataType; // результат
3
4 var named = attr.NamedArguments.FirstOrDefault(item => item.Key == "Type");
5 if (!named.Equals(default(KeyValuePair<string, TypedConstant>))) dataType = named.Value;
6 else
7 if (attribute.ConstructorArguments.Length == 2)
8 dataType = attribute.ConstructorArguments.Last();
9 else {
11 var arg = attribute.ConstructorArguments.FirstOrDefault();
12 if (!arg.Equals(default(TypedConstant)) && arg.Type.Name == "Type")
13 dataType = arg;
14 }
39
40. SemanticModel для анализа кода
• Предназначена для решения общих задач
• Получить список членов класса
• Получить список элементов namespace
• API для решения частных задач отсутствует
• Нельзя получить отдельный список свойств класса
• Нельзя получить список классов namespace
• Нет проверки примитивности типа
• Сложная работа с атрибутами
40
41. Extension метод: получить свойства класса
1 public static IEnumerable<IPropertySymbol>
2 GetProperties(this ITypeSymbol typeSymbol)
3 {
4 return typeSymbol
5 .GetMembers()
6 .Where(m => m.Kind == SymbolKind.Property)
7 .Cast<IPropertySymbol>();
8 }
41
42. SemanticModel vs Reflection
SemanticModel
± Нет API для решения частных
задач (extension method)
- Нет проверки примитивности
типа
- Сложное получение
параметров атрибутов
Reflection
+ Есть API для решения частных
задач
+ Есть проверка примитивности
типа
+ Простое получение
параметров атрибутов
42
43. Впечатление от Roslyn API
Мне хотелось бы видеть Roslyn API таким, что:
• Позволяет быстро решать простые задачи
• Допускает решение сложных задач
43
44. Заключение
SyntaxTree
• SyntaxRewriter – для точечных изменений
• Многословность (создать имя, а потом сам namespace)
• Решает общие задачи (нельзя добавить один модификатор)
• Есть удобные методы Parse*
• Форматирование в пакете Workspace
SemanticModel
• Решает общие задачи (нельзя получить свойства класса)
• Нет проверки типа на примитивность
• Неудобная работа с атрибутами
44
О Roslyn много говорят, отзывы положительные. Да, он поможет быстрее развивать C#.
Кроме прочего, код Roslyn открыт, и его функционал поставляется в виде пакетов.
Но что он даст широкому кругу разработчиков для решения прикладных задач, например кодогенерации и анализа кода
Это не такие распространенные задачи, как разработка справочников, но все же они боле актуальны, чем прикручивание новых фичей к C#
Я – послание,
Я проходил через, я научился, я хочу чтобы вы
Кто решал задачи: для вас этот доклад может оказаться полезным, потому, что вы узнаете что-то новое.
Вы увидите, как те же самые задачи можно решить с помощью другого инструмента. У вас расширится кругозор. И в следующий раз вы можете применить Roslyn для решения тех же задач.
Кто сделал сэмпл: я расскажу о нашем опыте и о тех граблях, которые мы собрали., чтобы вы на них уже не наступили
У кого реальный опыт: мы можем обменяться опытом, а возможно вы даже чем-то меня дополните.
Так ребята из MS ненавязчиво намекают, что SOAP – это не тренд. Особенно при разработке мобильных приложений.
Проблема: в фреймворке нет API, которое нужно для решения нашей задачи
Картинка?
Для других языков уже было реализовано. Например для питона и Java. Сейчас есть и для C#
Сказать про неизменяемость синтаксического дерева. Это позволяет одновременно анализировать синтаксическое дерево несколькими потоками
Позволяет решать простые задачи, вносить точечные правки
Пример – заменить имена классов в объявлении переменных на var
Для ситуации, когда код приходится перефигачивать очень сильно, он не подходит
Просто обход дерева без его изменения.
Пример – выбор дата контрактов и ServiceContract
Возможно стоит заменить на названия типов на var, чтобы влезало в одну строку
Возможно стоит заменить на названия типов на var, чтобы влезало в одну строку
Пример достаточно показателен.
Нельзя добавить классу один модификатор. Их можно добавить несколько.
Нельзя добавить один базовый класс, их можно добавить несколько.
Нужны ли примеры базовых классов и модификаторов
Полезно для разработчиков плагинов к студии. Но содержит полезную для кодогенерации фичу – форматирование.
Мы её как-то не сразу нашли, поэтому я о ней говорю, чтобы вы о ней знали и могли использовать.
Этот код не надо читать, здесь весь пример собран воедино
Кода много, и он сложный.
Самое плохое – он подходит только для атрибута XmlElement. Для другого атрибута этот код придется переписывать. А если атрибуту добавить или удалить конструкторов, этот код сломается.
Колбаса нравится не только из-за вкуса, но и из-за того, насколько хорошо она чистится.
Надеюсь, этот доклад оказался для вас полезным, и породил у вас какие-то практические мысли. Если у вас есть какие-то вопросы, буду рад на них ответить
Принцип 48 часов: чтобы информация не забылась, нужно что-то сделать.
Поиграйтесь с 2015 студией, попробуйте визуализировать SyntaxTree для вашего кода
Скачайте и посмотрите сэмплы, напишите простейший пример использовани Roslyn: как поменять название типа на var
Занесите Issue в GitHub.
Сами их реализуйте, сделайте Pull Request докажите насколько вы крутой программист.