Created
July 25, 2024 09:45
-
-
Save ThatRendle/93ccfb62f2e2f87137148b9aaa57f02f to your computer and use it in GitHub Desktop.
Reactive UI source generators
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Microsoft.CodeAnalysis; | |
namespace ReactiveUI.SourceGenerators; | |
[Generator] | |
public class AttributeGenerator : ISourceGenerator | |
{ | |
public void Initialize(GeneratorInitializationContext context) | |
{ | |
} | |
public void Execute(GeneratorExecutionContext context) | |
{ | |
context.AddSource("ReactiveUIPropertyAttribute.g.cs", AttributeCode); | |
} | |
// language=csharp | |
private const string AttributeCode = """ | |
namespace ReactiveUI | |
{ | |
using System; | |
[AttributeUsage(AttributeTargets.Field)] | |
internal class ReactivePropertyAttribute : Attribute | |
{ | |
public ReactivePropertyAttribute(string? name = null) | |
{ | |
Name = name; | |
} | |
public string? Name { get; } | |
} | |
[AttributeUsage(AttributeTargets.Method)] | |
internal class ReactiveCommandAttribute : Attribute | |
{ | |
public ReactiveCommandAttribute(string? name = null) | |
{ | |
Name = name; | |
} | |
public string? Name { get; } | |
public string? CanExecute { get; set; } | |
} | |
} | |
"""; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Text; | |
using Microsoft.CodeAnalysis; | |
using Microsoft.CodeAnalysis.CSharp; | |
using Microsoft.CodeAnalysis.CSharp.Syntax; | |
namespace ReactiveUI.SourceGenerators; | |
[Generator] | |
public class CommandGenerator : ISourceGenerator | |
{ | |
public void Initialize(GeneratorInitializationContext context) | |
{ | |
context.RegisterForSyntaxNotifications(() => new MyContextSyntaxReceiver()); | |
} | |
public void Execute(GeneratorExecutionContext context) | |
{ | |
if (context.SyntaxContextReceiver is not MyContextSyntaxReceiver syntaxContextReceiver) return; | |
var code = new StringBuilder(); | |
foreach (var namedTypeSymbol in syntaxContextReceiver.Symbols) | |
{ | |
code.AppendLine("#nullable enable"); | |
code.AppendLine("using System.ComponentModel;"); | |
code.AppendLine("using System.Reactive;"); | |
code.AppendLine("using System.Reactive.Linq;"); | |
code.AppendLine("using DynamicData.Binding;"); | |
code.AppendLine("using ReactiveUI;"); | |
code.AppendLine($"namespace {namedTypeSymbol.ContainingNamespace.ToDisplayString()}"); | |
code.AppendLine("{"); | |
code.AppendLine($" partial class {namedTypeSymbol.Name}"); | |
code.AppendLine(" {"); | |
foreach (var method in GetReactiveCommandMethods(namedTypeSymbol)) | |
{ | |
var attribute = method.GetAttributes() | |
.SingleOrDefault(a => a.AttributeClass?.Name is "ReactiveCommand" or "ReactiveCommandAttribute"); | |
if (attribute is null) continue; | |
var commandName = attribute.ConstructorArguments.FirstOrDefault().Value as string ?? $"{method.Name}Command"; | |
string? canExecuteMethodName = attribute.NamedArguments is { Length: 1 } && attribute.NamedArguments[0].Key == "CanExecute" | |
? attribute.NamedArguments[0].Value.Value as string | |
: null; | |
var canExecuteMethod = GetCanExecuteMethod(namedTypeSymbol, method.Name, canExecuteMethodName); | |
code.AppendLine($" public ReactiveCommand<Unit, Unit> {commandName} => _{commandName} ??= _Create{commandName}();"); | |
code.AppendLine(" [EditorBrowsable(EditorBrowsableState.Never)]"); | |
code.AppendLine($" private ReactiveCommand<Unit, Unit>? _{commandName};"); | |
code.AppendLine(" [EditorBrowsable(EditorBrowsableState.Never)]"); | |
code.AppendLine($" private ReactiveCommand<Unit, Unit> _Create{commandName}() =>"); | |
if (canExecuteMethod is not null) | |
{ | |
code.AppendLine($" ReactiveCommand.Create({method.Name}, this.WhenAnyPropertyChanged().Select(_ => {canExecuteMethod.Name}()));"); | |
} | |
else | |
{ | |
code.AppendLine($" ReactiveCommand.Create({method.Name});"); | |
} | |
} | |
code.AppendLine(" }"); | |
code.AppendLine("}"); | |
context.AddSource($"{namedTypeSymbol.Name}.ReactiveCommands.g.cs", code.ToString()); | |
code.Clear(); | |
} | |
} | |
private class MyContextSyntaxReceiver : ISyntaxContextReceiver | |
{ | |
public List<INamedTypeSymbol> Symbols { get; } = new(); | |
public void OnVisitSyntaxNode(GeneratorSyntaxContext context) | |
{ | |
if (context.Node is not ClassDeclarationSyntax cds) return; | |
if (!cds.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword))) return; | |
if (ModelExtensions.GetDeclaredSymbol(context.SemanticModel, cds) is not INamedTypeSymbol namedTypeSymbol) return; | |
if (!namedTypeSymbol.DerivesFrom("ReactiveUI", "ReactiveObject")) return; | |
if (GetReactiveCommandMethods(namedTypeSymbol).Any()) | |
{ | |
Symbols.Add(namedTypeSymbol); | |
} | |
} | |
} | |
private static IEnumerable<IMethodSymbol> GetReactiveCommandMethods(INamedTypeSymbol namedTypeSymbol) | |
{ | |
return namedTypeSymbol.GetMembers().OfType<IMethodSymbol>() | |
.Where(m => m.GetAttributes() | |
.Any(a => a.AttributeClass?.Name is "ReactiveCommand" or "ReactiveCommandAttribute")); | |
} | |
private static IMethodSymbol? GetCanExecuteMethod(INamedTypeSymbol namedTypeSymbol, string commandMethodName, string? canExecuteMethodName = null) | |
{ | |
canExecuteMethodName ??= $"Can{commandMethodName}"; | |
return namedTypeSymbol.GetMembers().OfType<IMethodSymbol>() | |
.FirstOrDefault(m => m.Name == canExecuteMethodName && m.Parameters.Length == 0); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Text; | |
using Microsoft.CodeAnalysis; | |
using Microsoft.CodeAnalysis.CSharp; | |
using Microsoft.CodeAnalysis.CSharp.Syntax; | |
namespace ReactiveUI.SourceGenerators; | |
[Generator] | |
public class PropertyGenerator : ISourceGenerator | |
{ | |
public void Initialize(GeneratorInitializationContext context) | |
{ | |
context.RegisterForSyntaxNotifications(() => new MyContextSyntaxReceiver()); | |
} | |
public void Execute(GeneratorExecutionContext context) | |
{ | |
if (context.SyntaxContextReceiver is not MyContextSyntaxReceiver syntaxContextReceiver) return; | |
var code = new StringBuilder(); | |
foreach (var namedTypeSymbol in syntaxContextReceiver.Symbols) | |
{ | |
code.AppendLine("#nullable enable"); | |
code.AppendLine("using ReactiveUI;"); | |
code.AppendLine($"namespace {namedTypeSymbol.ContainingNamespace.ToDisplayString()}"); | |
code.AppendLine("{"); | |
code.AppendLine($" partial class {namedTypeSymbol.Name}"); | |
code.AppendLine(" {"); | |
foreach (var field in GetReactivePropertyFields(namedTypeSymbol)) | |
{ | |
var attribute = field.GetAttributes() | |
.SingleOrDefault(a => a.AttributeClass?.Name is "ReactiveProperty" or "ReactivePropertyAttribute"); | |
if (attribute is null) continue; | |
var propertyName = GetPropertyName(field, attribute); | |
code.AppendLine( | |
$" public {field.Type.ToDisplayString()} {propertyName} {{ get => {field.Name}; set => this.RaiseAndSetIfChanged(ref {field.Name}, value); }}"); | |
} | |
code.AppendLine(" }"); | |
code.AppendLine("}"); | |
context.AddSource($"{namedTypeSymbol.Name}.ReactiveProperties.g.cs", code.ToString()); | |
code.Clear(); | |
} | |
} | |
private static string GetPropertyName(IFieldSymbol field, AttributeData? attribute) | |
{ | |
if (attribute?.ConstructorArguments.Length > 0) | |
{ | |
if (attribute.ConstructorArguments[0].Value is string { Length: > 0 } name) | |
{ | |
return name; | |
} | |
} | |
return AutoName(field.Name.AsSpan()); | |
} | |
private class MyContextSyntaxReceiver : ISyntaxContextReceiver | |
{ | |
public List<INamedTypeSymbol> Symbols { get; } = new(); | |
public void OnVisitSyntaxNode(GeneratorSyntaxContext context) | |
{ | |
if (context.Node is not ClassDeclarationSyntax cds) return; | |
if (!cds.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword))) return; | |
if (ModelExtensions.GetDeclaredSymbol(context.SemanticModel, cds) is not INamedTypeSymbol namedTypeSymbol) return; | |
if (!namedTypeSymbol.DerivesFrom("ReactiveUI", "ReactiveObject")) return; | |
if (GetReactivePropertyFields(namedTypeSymbol).Any()) | |
{ | |
Symbols.Add(namedTypeSymbol); | |
} | |
} | |
} | |
private static IEnumerable<IFieldSymbol> GetReactivePropertyFields(INamedTypeSymbol namedTypeSymbol) | |
{ | |
return namedTypeSymbol.GetMembers().OfType<IFieldSymbol>() | |
.Where(f => | |
f.GetAttributes() | |
.Any(a => a.AttributeClass?.Name == "ReactiveProperty")); | |
} | |
private static string AutoName(ReadOnlySpan<char> fieldName) | |
{ | |
fieldName = fieldName.TrimStart('_'); | |
Span<char> span = stackalloc char[fieldName.Length]; | |
fieldName.CopyTo(span); | |
span[0] = char.ToUpperInvariant(span[0]); | |
return span.ToString(); | |
} | |
} | |
internal static class NamedTypeSymbolExtensions | |
{ | |
public static bool DerivesFrom(this INamedTypeSymbol namedTypeSymbol, string ns, string name) | |
{ | |
for (var baseType = namedTypeSymbol.BaseType; baseType is not null; baseType = baseType.BaseType) | |
{ | |
if (baseType.ContainingNamespace.ToDisplayString() == ns && baseType.Name == name) return true; | |
} | |
return false; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment