[.NET, Visual Studio, C#, Разработка под Linux, Разработка под Windows] Заменяем события C# на Reactive Extensions с помощью кодогенерации
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Здравствуйте, меня зовут Иван и я разработчик. Недавно прошла приуроченная к выходу .NET 5 конференция .NETConf 2020. На которой один из докладчиков рассказывал про C# Source Generators. Поискав на youtube нашел еще неплохое видео по этой теме. Советую их посмотреть. В них показывается как во время написания кода разработчиком, генерируется код, а InteliSense тут же подхватывает сгенерированный код, предлагает сгенерированные методы и свойства, а компилятор не ругается на их отсутствие. На мой взгляд, это хорошая возможность для расширения возможностей языка и я попробую это продемонстрировать.ИдеяВсе же знают LINQ? Так вот для событий есть аналогичная библиотека Reactive Extensions, которая позволяет в том же виде, что и LINQобрабатывать события. Проблема в том, что чтобы пользоваться Reactive Extensions надо и события оформить в виде Reactive Extensions, а так как все события, в стандартных библиотеках, написаны в стандартном виде то и Reactive Extensions использовать не удобно. Есть костыль, который преобразует стандартные события C# в вид Reactive Extensions. Выглядит он так. Допустим есть класс с каким-то событием:
public partial class Example
{
public event Action<int, string, bool> ActionEvent;
}
Чтобы этим событием можно было пользоваться в стиле Reactive Extensions необходимо написать метод расширения вида:
public static IObservable<(int, string, bool)> RxActionEvent(this TestConsoleApp.Example obj)
{
if (obj == null) throw new ArgumentNullException(nameof(obj));
return Observable.FromEvent<System.Action<int, string, bool>, (int, string, bool)>(
conversion => (obj0, obj1, obj2) => conversion((obj0, obj1, obj2)),
h => obj.ActionEvent += h,
h => obj.ActionEvent -= h);
}
И после этого можно воспользоваться всеми плюсами Reactive Extensions, например, вот так:
var example = new Example();
example.RxActionEvent().Where(obj => obj.Item1 > 10).Take(1).Subscribe((obj)=> { /* some action */});
Так вот, идея состоит в том, чтобы костыль этот генерировался сам, а методами можно было пользоваться из InteliSense при разработке.Задача1) Если в коде после установленного маркера «.» использующегося для обращения к члену класса идет полноценное обращение к методу начинающемуся на «Rx», например, example.RxActionEvent(), а имя метода совпадает с именем одного из событий класса, например, у класса есть событие Action ActionEvent, а в коде написано .RxActionEvent(), должен сгенерироваться следующий код:
public static IObservable<(System.Int32 Item1Int32, System.String Item2String, System.Boolean Item3Boolean)> RxActionEvent(this TestConsoleApp.Example obj)
{
if (obj == null) throw new ArgumentNullException(nameof(obj));
return Observable.FromEvent<System.Action<System.Int32, System.String, System.Boolean>, (System.Int32 Item1Int32, System.String Item2String, System.Boolean
Item3Boolean)>(
conversion => (obj0, obj1, obj2) => conversion((obj0, obj1, obj2)),
h => obj.ActionEvent += h,
h => obj.ActionEvent -= h);
}
2) InteliSense должен подсказывать имя метода до его генерации.Настройка проектовДля начала надо создать 2 проекта первый для самого генератора второй для тестов и отладки.Проект генератора выглядит следующим образом:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>preview</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.8.0" />
</ItemGroup>
</Project>
Обратите внимание, что проект должен быть netstandard2.0 и включать 2 пакета Microsoft.CodeAnalysis.Analyzers и Microsoft.CodeAnalysis.CSharp.Workspaces.Проектом для тестов будет простой консольный проект и выглядит так:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<LangVersion>preview</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Reactive" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SourceGenerator\RxSourceGenerator.csproj" ReferenceOutputAssembly="false" OutputItemType="Analyzer" />
</ItemGroup>
</Project>
Обратите внимание как добавлен проект генератора в тестовый проект, иначе работать не будет:
<ProjectReference Include="..\SourceGenerator\RxSourceGenerator.csproj" ReferenceOutputAssembly="false" OutputItemType="Analyzer" />
Разработка генератораСам генератор должен быть помечен атрибутом [Generator] и реализовывать ISourceGenerator:
[Generator]
public class RxGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context) { }
public void Execute(GeneratorExecutionContext context) { }
}
Mетод Initializeиспользуется для инициализации генератора, а Executeдля генерации исходного кода.В методе Initializeмы можем зарегистрировать ISyntaxReceiver.Логика, здесь следующая:
- файл парсится на синтаксис->
- каждый синтаксис в файле передается в ISyntaxReceiver->
- в ISyntaxReceiverнадо отобрать тот синтаксис, который нужен для генерации кода->
- в методе Executeждем когда придет ISyntaxReceiver, и на его базе генерируем код.
Если это звучит сложно, то код выглядит просто:
[Generator]
public class RxGenerator : ISourceGenerator
{
private const string firstText = @"using System; using System.Reactive.Linq; namespace RxGenerator{}";
public void Initialize(GeneratorInitializationContext context)
{
// Регистрируем ISyntaxReceiver
context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
}
public void Execute(GeneratorExecutionContext context)
{
if (context.SyntaxReceiver is not SyntaxReceiver receiver) return;
// Добавляем новый файл с именем "RxGenerator.cs" и текстом, что в firstText
context.AddSource("RxGenerator.cs", fitstText);
}
class SyntaxReceiver : ISyntaxReceiver
{
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
// здесь надо отобрать тот синтаксис, который нужен для генерации кода.
}
}
}
Если на данной стадии скомпилировать проект генератора и перезагрузить VS, то в код тестового проекта можно добавить using RxGenerator; и на него не будет ругаться VS.Отбор синтаксиса в ISyntaxReceiverВ методе OnVisitSyntaxNode находим синтаксис MemberAccessExpressionSyntax.
private class SyntaxReceiver : ISyntaxReceiver
{
public List<MemberAccessExpressionSyntax> GenerateCandidates { get; } =
new List<MemberAccessExpressionSyntax>();
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
if (!(syntaxNode is MemberAccessExpressionSyntax syntax)) return;
if (syntax.HasTrailingTrivia || syntax.Name.IsMissing) return;
if (!syntax.Name.ToString().StartsWith("Rx")) return;
GenerateCandidates.Add(syntax);
}
}
Здесь:
- syntax.Name.IsMissing это случай когда поставили точку и ничего не написали
- syntax.HasTrailingTrivia это случай когда поставили точку и что-то начали печатать
- !syntax.Name.ToString().StartsWith("Rx") это случай когда поставили точку написали метод но метод не начинается с "Rx"
Эти случаи надо исключить, остальное попадает в список кандидатов на генерацию кода.Получение всей необходимой информации для генерацииЧтобы сгенерировать метод расширения необходима следующая информация:
- Тип класса, для которого генерируются методы
- Полный тип события. Например, System.Action<System.Int32, System.String, System.Boolean,xSouceGeneratorXUnitTests.SomeEventArgs>
- Список всех аргументов делегата события
Получения этой информации рассмотрим на коде:
private static IEnumerable<(string ClassType, string EventName, string EventType, List<string> ArgumentTypes, bool IsStub)>
GetExtensionMethodInfo(GeneratorExecutionContext context, SyntaxReceiver receiver)
{
HashSet<(string ClassType, string EventName)>
hashSet = new HashSet<(string ClassType, string EventName)>();
foreach (MemberAccessExpressionSyntax syntax in receiver.GenerateCandidates)
{
SemanticModel model = context.Compilation.GetSemanticModel(syntax.SyntaxTree);
ITypeSymbol? typeSymbol = model.GetSymbolInfo(syntax.Expression).Symbol switch
{
IMethodSymbol s => s.ReturnType,
ILocalSymbol s => s.Type,
IPropertySymbol s => s.Type,
IFieldSymbol s => s.Type,
IParameterSymbol s => s.Type,
_ => null
};
if (typeSymbol == null) continue;
...
Для того чтобы получить тип класса необходимо сначала получить SemanticModel. Из неё получить информацию о объекте для которого генерируются методы. И вот оттуда получаем тип ITypeSymbol. А из ITypeSymbol можно получить остальную информацию.
...
string eventName = syntax.Name.ToString().Substring(2);
if (!(typeSymbol.GetMembersOfType<IEventSymbol>().FirstOrDefault(m => m.Name == eventName) is { } ev)
) continue;
if (!(ev.Type is INamedTypeSymbol namedTypeSymbol)) continue;
if (namedTypeSymbol.DelegateInvokeMethod == null) continue;
if (!hashSet.Add((typeSymbol.ToString(), ev.Name))) continue;
string fullType = namedTypeSymbol.ToDisplayString(SymbolDisplayFormat);
List<string> typeArguments = namedTypeSymbol.DelegateInvokeMethod.Parameters
.Select(m => m.Type.ToDisplayString(SymbolDisplayFormat)).ToList();
yield return (typeSymbol.ToString(), ev.Name, fullType, typeArguments, false);
}
}
Здесь стоит отдельно обратить внимание на:
string fullType = namedTypeSymbol.ToDisplayString(symbolDisplayFormat);
SymbolDisplayFormat это такой хитрый класс SymbolDisplayFormatкоторый объясняет методу ToDisplayString() в каком виде необходимо выдать информацию. Без него метод ToDisplayString()вместо:
System.Action<System.Int32, System.String, System.Boolean, RxSouceGeneratorXUnitTests.SomeEventArgs>
вернёт
Action<int, string, bool, SomeEventArgs>
То есть в сокращенном виде.Также интересно место:
List<string> typeArguments = namedTypeSymbol.DelegateInvokeMethod.Parameters.Select(m => m.Type.ToDisplayString(SymbolDisplayFormat)).ToList();
Здесь получаются типы аргументов делегата события.Далее в StringBuilder из полученной информации собираем статический класс, который содержит все методы расширения, которые необходимо. Полный код метода Execute:Spoiler
public void Execute(GeneratorExecutionContext context)
{
if (!(context.SyntaxReceiver is SyntaxReceiver receiver)) return;
if (!(receiver.GenerateCandidates.Any()))
{
context.AddSource("RxGenerator.cs", startText);
return;
}
StringBuilder sb = new StringBuilder();
sb.AppendLine("using System;");
sb.AppendLine("using System.Reactive.Linq;");
sb.AppendLine("namespace RxMethodGenerator{");
sb.AppendLine(" public static class RxGeneratedMethods{");
foreach ((string classType, string eventName, string eventType, List<string> argumentTypes, bool isStub) in GetExtensionMethodInfo(context,
receiver))
{
string tupleTypeStr;
string conversionStr;
switch (argumentTypes.Count)
{
case 0:
tupleTypeStr = classType;
conversionStr = "conversion => () => conversion(obj),";
break;
case 1:
tupleTypeStr = argumentTypes.First();
conversionStr = "conversion => obj1 => conversion(obj1),";
break;
default:
tupleTypeStr =
$"({string.Join(", ", argumentTypes.Select((x, i) => $"{x} Item{i + 1}{x.Split('.').Last()}"))})";
string objStr = string.Join(", ", argumentTypes.Select((x, i) => $"obj{i}"));
conversionStr = $"conversion => ({objStr}) => conversion(({objStr})),";
break;
}
sb.AppendLine(
@$" public static IObservable<{tupleTypeStr}> Rx{eventName}(this {classType} obj)");
sb.AppendLine(@" {");
if (isStub)
{
sb.AppendLine(" throw new Exception('RxGenerator stub');");
}
else
{
sb.AppendLine(" if (obj == null) throw new ArgumentNullException(nameof(obj));");
sb.AppendLine(@$" return Observable.FromEvent<{eventType}, {tupleTypeStr}>(");
sb.AppendLine(@$" {conversionStr}");
sb.AppendLine(@$" h => obj.{eventName} += h,");
sb.AppendLine(@$" h => obj.{eventName} -= h);");
}
sb.AppendLine(" }");
}
sb.AppendLine(" }");
sb.AppendLine("}");
context.AddSource("RxGenerator.cs", sb.ToString());
}
Добавление в InteliSense метода расширение до его генерацииНа текущей стадии после установленного маркера «.» InteliSense нам буде подсказывать имя метода расширения только если генератор уже его сгенерировал. Но хотелось бы чтобы подсказка была всегда. Я пробовал при установки маркера «.» получать все события из объекта и для них генерировать методы расширения. Это работает, но разработчики MS советуют так не делать и обещают добавить функционал обработки редактируемого кода в будущем. Поэтому я пошел другим путем.На самом деле можно написать CompletionProviderэто как раз действия InteliSense после установленного маркера «.». С недавних пор его можно поставлять через NuGet, так что его можно положить рядом с генератором.Итак по порядку.В CompletionProviderесть метод, который отбирает триггеры, на которые отработает CompletionProvider:
public override bool ShouldTriggerCompletion(SourceText text, int caretPosition, CompletionTrigger trigger, OptionSet options)
{
switch (trigger.Kind)
{
case CompletionTriggerKind.Insertion:
int insertedCharacterPosition = caretPosition - 1;
if (insertedCharacterPosition <= 0) return false;
char ch = text[insertedCharacterPosition];
char previousCh = text[insertedCharacterPosition - 1];
return ch == '.' && !char.IsWhiteSpace(previousCh) && previousCh != '\t' && previousCh != '\r' && previousCh != '\n';
default:
return false;
}
}
В данном случае отбирается установленный маркер «.» если перед ним есть какой-то символ. Если метод вернет True то сработает следующий метод, в котором подготавливаются элементы InteliSense:
public override async Task ProvideCompletionsAsync(CompletionContext context)
{
SyntaxNode? syntaxNode = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
if (!(syntaxNode?.FindNode(context.CompletionListSpan) is ExpressionStatementSyntax
expressionStatementSyntax)) return;
if (!(expressionStatementSyntax.Expression is MemberAccessExpressionSyntax syntax)) return;
if (!(await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false) is { }
model)) return;
ITypeSymbol? typeSymbol = model.GetSymbolInfo(syntax.Expression).Symbol switch
{
IMethodSymbol s => s.ReturnType,
ILocalSymbol s => s.Type,
IPropertySymbol s => s.Type,
IFieldSymbol s => s.Type,
IParameterSymbol s => s.Type,
_ => null
};
if (typeSymbol == null) return;
foreach (IEventSymbol ev in typeSymbol.GetMembersOfType<IEventSymbol>())
{
...
// Создаем и добавляем элемент InteliSense
CompletionItem item = CompletionItem.Create($"Rx{ev.Name}");
context.AddItem(item);
}
}
Этот метод частично скопирован из генератора, описанного выше, только здесь находим все события объекта и их параметры.После чего вызывается метод, который добавляет описание методу при наведении на него курсора в InteliSense:
public override Task<CompletionDescription> GetDescriptionAsync(Document document, CompletionItem item, CancellationToken cancellationToken)
{
return Task.FromResult(CompletionDescription.FromText("Описание метода"));
}
Если в InteliSense выбрать созданный элемент сработает следующий метод, который непосредственно заменяет все, что было набрано после маркера «.» на выбранный метод:
public override async Task<CompletionChange> GetChangeAsync(Document document, CompletionItem item,
char? commitKey, CancellationToken cancellationToken)
{
string newText = $".{item.DisplayText}()";
TextSpan newSpan = new TextSpan(item.Span.Start - 1, 1);
TextChange textChange = new TextChange(newSpan, newText);
return await Task.FromResult(CompletionChange.Create(textChange));
}
Всё!Где и как это работаетВсе это работает в Visual Studio №16.8.3. На GitHabесть гифка демонстрирующая как это выглядит в Visual Studio. В Rider и ReSharper пока не работает. Так что не забудьте выключить ReSharper перед экспериментами.Сами генераторы исходного кода работают на проектах простой консольки или библиотеках, это я проверял. На WPF не работает, этот баг описан на GitHab Roslyn.Для CompletionProviderвсе работает если его собрать как Vsix расширение. Если как NuGet работает только само добавление метода. Описание метода не работает. Я сделал чтобы автоматом еще using добавлялись, но это тоже пока не работает для NuGet.Как это все отлаживатьГенератор отлаживать можно добавив в метод Initialize строчку Debugger.Launch(); и перезапустить VS
public override async Task<CompletionChange> GetChangeAsync(Document document, CompletionItem item,
char? commitKey, CancellationToken cancellationToken)
{
public void Initialize(GeneratorInitializationContext context)
{
#if (DEBUG)
Debugger.Launch();
#endif
context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
}
Вообще отладка генераторов исходного кода пока очень сырая. Если что-то непонятное сразу перезагружайте VS, скорее всего поможет. Для отладки CompletionProviderпроще всего использовать шаблон в VS «Analyzer with code Fix». Создать проекты по шаблону, после чего запускать проект Vsix. Он буде загружать новую студию с подключенным CompletionProvider как расширение, в котором можно нормально отлаживать. Краткий выводКод генератора уместился в 140 строк. За эти 140 строк получилось изменить синтаксис языка, избавится от событий заменив их на Reactive Extensions с более удобным, на мой взгляд, подходом. Я думаю, что технология генераторов исходного кода сильно изменит подход к разработке библиотек и расширений. СсылкиNuGetGitHab
===========
Источник:
habr.com
===========
Похожие новости:
- [.NET] Как я на собеседование готовился №2
- [Настройка Linux, Open source, Разработка под Linux] Состоялся релиз ядра Linux 5.10
- [Хакатоны, Конференции] Digital-мероприятия в Москве c 14 по 20 декабря
- [.NET] Как я на собеседование готовился №1
- [.NET, C#] Авторизация из приложения C# на портале BlaBlaCar.ru
- [SaaS / S+S, C#, IT-компании] UiPath release notes: последние апдейты и акцент на работе в облаках
- [Разработка под Windows, IT-компании] Microsoft добавила поддержку эмуляции x64 в Windows 10 на ARM
- [.NET, C++, Обработка изображений, C#] Сравниваем производительность C# и C++ в задачах обработки изображений
- [Go, Разработка под Linux] Оптимизация размера Go-бинарника
- [Разработка под Linux, Игры и игровые приставки] Проект Proton запускает Cyberpunk 2077 на Linux
Теги для поиска: #_.net, #_visual_studio, #_c#, #_razrabotka_pod_linux (Разработка под Linux), #_razrabotka_pod_windows (Разработка под Windows), #_c#, #_.net, #_reactivex, #_reactive_extensions, #_events, #_c#_source_generators, #_completionprovider, #_.net, #_visual_studio, #_c#, #_razrabotka_pod_linux (
Разработка под Linux
), #_razrabotka_pod_windows (
Разработка под Windows
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 17:19
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Здравствуйте, меня зовут Иван и я разработчик. Недавно прошла приуроченная к выходу .NET 5 конференция .NETConf 2020. На которой один из докладчиков рассказывал про C# Source Generators. Поискав на youtube нашел еще неплохое видео по этой теме. Советую их посмотреть. В них показывается как во время написания кода разработчиком, генерируется код, а InteliSense тут же подхватывает сгенерированный код, предлагает сгенерированные методы и свойства, а компилятор не ругается на их отсутствие. На мой взгляд, это хорошая возможность для расширения возможностей языка и я попробую это продемонстрировать.ИдеяВсе же знают LINQ? Так вот для событий есть аналогичная библиотека Reactive Extensions, которая позволяет в том же виде, что и LINQобрабатывать события. Проблема в том, что чтобы пользоваться Reactive Extensions надо и события оформить в виде Reactive Extensions, а так как все события, в стандартных библиотеках, написаны в стандартном виде то и Reactive Extensions использовать не удобно. Есть костыль, который преобразует стандартные события C# в вид Reactive Extensions. Выглядит он так. Допустим есть класс с каким-то событием: public partial class Example
{ public event Action<int, string, bool> ActionEvent; } public static IObservable<(int, string, bool)> RxActionEvent(this TestConsoleApp.Example obj)
{ if (obj == null) throw new ArgumentNullException(nameof(obj)); return Observable.FromEvent<System.Action<int, string, bool>, (int, string, bool)>( conversion => (obj0, obj1, obj2) => conversion((obj0, obj1, obj2)), h => obj.ActionEvent += h, h => obj.ActionEvent -= h); } var example = new Example();
example.RxActionEvent().Where(obj => obj.Item1 > 10).Take(1).Subscribe((obj)=> { /* some action */}); public static IObservable<(System.Int32 Item1Int32, System.String Item2String, System.Boolean Item3Boolean)> RxActionEvent(this TestConsoleApp.Example obj)
{ if (obj == null) throw new ArgumentNullException(nameof(obj)); return Observable.FromEvent<System.Action<System.Int32, System.String, System.Boolean>, (System.Int32 Item1Int32, System.String Item2String, System.Boolean Item3Boolean)>( conversion => (obj0, obj1, obj2) => conversion((obj0, obj1, obj2)), h => obj.ActionEvent += h, h => obj.ActionEvent -= h); } <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <LangVersion>preview</LangVersion> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.1"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.8.0" /> </ItemGroup> </Project> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>netcoreapp3.1</TargetFramework> <LangVersion>preview</LangVersion> </PropertyGroup> <ItemGroup> <PackageReference Include="System.Reactive" Version="5.0.0" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\SourceGenerator\RxSourceGenerator.csproj" ReferenceOutputAssembly="false" OutputItemType="Analyzer" /> </ItemGroup> </Project> <ProjectReference Include="..\SourceGenerator\RxSourceGenerator.csproj" ReferenceOutputAssembly="false" OutputItemType="Analyzer" />
[Generator]
public class RxGenerator : ISourceGenerator { public void Initialize(GeneratorInitializationContext context) { } public void Execute(GeneratorExecutionContext context) { } }
[Generator]
public class RxGenerator : ISourceGenerator { private const string firstText = @"using System; using System.Reactive.Linq; namespace RxGenerator{}"; public void Initialize(GeneratorInitializationContext context) { // Регистрируем ISyntaxReceiver context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); } public void Execute(GeneratorExecutionContext context) { if (context.SyntaxReceiver is not SyntaxReceiver receiver) return; // Добавляем новый файл с именем "RxGenerator.cs" и текстом, что в firstText context.AddSource("RxGenerator.cs", fitstText); } class SyntaxReceiver : ISyntaxReceiver { public void OnVisitSyntaxNode(SyntaxNode syntaxNode) { // здесь надо отобрать тот синтаксис, который нужен для генерации кода. } } } private class SyntaxReceiver : ISyntaxReceiver
{ public List<MemberAccessExpressionSyntax> GenerateCandidates { get; } = new List<MemberAccessExpressionSyntax>(); public void OnVisitSyntaxNode(SyntaxNode syntaxNode) { if (!(syntaxNode is MemberAccessExpressionSyntax syntax)) return; if (syntax.HasTrailingTrivia || syntax.Name.IsMissing) return; if (!syntax.Name.ToString().StartsWith("Rx")) return; GenerateCandidates.Add(syntax); } }
private static IEnumerable<(string ClassType, string EventName, string EventType, List<string> ArgumentTypes, bool IsStub)>
GetExtensionMethodInfo(GeneratorExecutionContext context, SyntaxReceiver receiver) { HashSet<(string ClassType, string EventName)> hashSet = new HashSet<(string ClassType, string EventName)>(); foreach (MemberAccessExpressionSyntax syntax in receiver.GenerateCandidates) { SemanticModel model = context.Compilation.GetSemanticModel(syntax.SyntaxTree); ITypeSymbol? typeSymbol = model.GetSymbolInfo(syntax.Expression).Symbol switch { IMethodSymbol s => s.ReturnType, ILocalSymbol s => s.Type, IPropertySymbol s => s.Type, IFieldSymbol s => s.Type, IParameterSymbol s => s.Type, _ => null }; if (typeSymbol == null) continue; ... ...
string eventName = syntax.Name.ToString().Substring(2); if (!(typeSymbol.GetMembersOfType<IEventSymbol>().FirstOrDefault(m => m.Name == eventName) is { } ev) ) continue; if (!(ev.Type is INamedTypeSymbol namedTypeSymbol)) continue; if (namedTypeSymbol.DelegateInvokeMethod == null) continue; if (!hashSet.Add((typeSymbol.ToString(), ev.Name))) continue; string fullType = namedTypeSymbol.ToDisplayString(SymbolDisplayFormat); List<string> typeArguments = namedTypeSymbol.DelegateInvokeMethod.Parameters .Select(m => m.Type.ToDisplayString(SymbolDisplayFormat)).ToList(); yield return (typeSymbol.ToString(), ev.Name, fullType, typeArguments, false); } } string fullType = namedTypeSymbol.ToDisplayString(symbolDisplayFormat);
System.Action<System.Int32, System.String, System.Boolean, RxSouceGeneratorXUnitTests.SomeEventArgs>
Action<int, string, bool, SomeEventArgs>
List<string> typeArguments = namedTypeSymbol.DelegateInvokeMethod.Parameters.Select(m => m.Type.ToDisplayString(SymbolDisplayFormat)).ToList();
public void Execute(GeneratorExecutionContext context)
{ if (!(context.SyntaxReceiver is SyntaxReceiver receiver)) return; if (!(receiver.GenerateCandidates.Any())) { context.AddSource("RxGenerator.cs", startText); return; } StringBuilder sb = new StringBuilder(); sb.AppendLine("using System;"); sb.AppendLine("using System.Reactive.Linq;"); sb.AppendLine("namespace RxMethodGenerator{"); sb.AppendLine(" public static class RxGeneratedMethods{"); foreach ((string classType, string eventName, string eventType, List<string> argumentTypes, bool isStub) in GetExtensionMethodInfo(context, receiver)) { string tupleTypeStr; string conversionStr; switch (argumentTypes.Count) { case 0: tupleTypeStr = classType; conversionStr = "conversion => () => conversion(obj),"; break; case 1: tupleTypeStr = argumentTypes.First(); conversionStr = "conversion => obj1 => conversion(obj1),"; break; default: tupleTypeStr = $"({string.Join(", ", argumentTypes.Select((x, i) => $"{x} Item{i + 1}{x.Split('.').Last()}"))})"; string objStr = string.Join(", ", argumentTypes.Select((x, i) => $"obj{i}")); conversionStr = $"conversion => ({objStr}) => conversion(({objStr})),"; break; } sb.AppendLine( @$" public static IObservable<{tupleTypeStr}> Rx{eventName}(this {classType} obj)"); sb.AppendLine(@" {"); if (isStub) { sb.AppendLine(" throw new Exception('RxGenerator stub');"); } else { sb.AppendLine(" if (obj == null) throw new ArgumentNullException(nameof(obj));"); sb.AppendLine(@$" return Observable.FromEvent<{eventType}, {tupleTypeStr}>("); sb.AppendLine(@$" {conversionStr}"); sb.AppendLine(@$" h => obj.{eventName} += h,"); sb.AppendLine(@$" h => obj.{eventName} -= h);"); } sb.AppendLine(" }"); } sb.AppendLine(" }"); sb.AppendLine("}"); context.AddSource("RxGenerator.cs", sb.ToString()); } public override bool ShouldTriggerCompletion(SourceText text, int caretPosition, CompletionTrigger trigger, OptionSet options)
{ switch (trigger.Kind) { case CompletionTriggerKind.Insertion: int insertedCharacterPosition = caretPosition - 1; if (insertedCharacterPosition <= 0) return false; char ch = text[insertedCharacterPosition]; char previousCh = text[insertedCharacterPosition - 1]; return ch == '.' && !char.IsWhiteSpace(previousCh) && previousCh != '\t' && previousCh != '\r' && previousCh != '\n'; default: return false; } } public override async Task ProvideCompletionsAsync(CompletionContext context)
{ SyntaxNode? syntaxNode = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); if (!(syntaxNode?.FindNode(context.CompletionListSpan) is ExpressionStatementSyntax expressionStatementSyntax)) return; if (!(expressionStatementSyntax.Expression is MemberAccessExpressionSyntax syntax)) return; if (!(await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false) is { } model)) return; ITypeSymbol? typeSymbol = model.GetSymbolInfo(syntax.Expression).Symbol switch { IMethodSymbol s => s.ReturnType, ILocalSymbol s => s.Type, IPropertySymbol s => s.Type, IFieldSymbol s => s.Type, IParameterSymbol s => s.Type, _ => null }; if (typeSymbol == null) return; foreach (IEventSymbol ev in typeSymbol.GetMembersOfType<IEventSymbol>()) { ... // Создаем и добавляем элемент InteliSense CompletionItem item = CompletionItem.Create($"Rx{ev.Name}"); context.AddItem(item); } } public override Task<CompletionDescription> GetDescriptionAsync(Document document, CompletionItem item, CancellationToken cancellationToken)
{ return Task.FromResult(CompletionDescription.FromText("Описание метода")); } public override async Task<CompletionChange> GetChangeAsync(Document document, CompletionItem item,
char? commitKey, CancellationToken cancellationToken) { string newText = $".{item.DisplayText}()"; TextSpan newSpan = new TextSpan(item.Span.Start - 1, 1); TextChange textChange = new TextChange(newSpan, newText); return await Task.FromResult(CompletionChange.Create(textChange)); } public override async Task<CompletionChange> GetChangeAsync(Document document, CompletionItem item,
char? commitKey, CancellationToken cancellationToken) { public void Initialize(GeneratorInitializationContext context) { #if (DEBUG) Debugger.Launch(); #endif context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); } =========== Источник: habr.com =========== Похожие новости:
Разработка под Linux ), #_razrabotka_pod_windows ( Разработка под Windows ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 17:19
Часовой пояс: UTC + 5