Skip to content

Instantly share code, notes, and snippets.

@ThatRendle
Created July 25, 2024 09:45
Show Gist options
  • Save ThatRendle/93ccfb62f2e2f87137148b9aaa57f02f to your computer and use it in GitHub Desktop.
Save ThatRendle/93ccfb62f2e2f87137148b9aaa57f02f to your computer and use it in GitHub Desktop.
Reactive UI source generators
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; }
}
}
""";
}
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);
}
}
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