mirror of
https://github.com/AlexMacocian/WpfExtended.git
synced 2026-07-25 08:22:08 +00:00
Switch to Incremental Generators
Implement INotifyPropertyChanged Generator
This commit is contained in:
@@ -2,194 +2,179 @@
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using Sybil;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Extensions.Templates;
|
||||
|
||||
namespace System.Extensions
|
||||
namespace System.Extensions;
|
||||
#nullable enable
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
public class DependencyPropertyGenerator : IIncrementalGenerator
|
||||
{
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
public class DependencyPropertyGenerator : ISourceGenerator
|
||||
private const string DependencyPropertyNamespace = "System.Windows";
|
||||
private const string AttributeNamespace = "System.Windows.Extensions";
|
||||
private const string AttributeName = "GenerateDependencyPropertyAttribute";
|
||||
private const string AttributeShortName = "GenerateDependencyProperty";
|
||||
private const string DependencyPropertySuffix = "Property";
|
||||
private const string DependencyProperty = "DependencyProperty";
|
||||
private const string Register = "Register";
|
||||
private const string Partial = "partial";
|
||||
private const string Public = "public";
|
||||
private const string Static = "static";
|
||||
private const string Readonly = "readonly";
|
||||
private const string InitialValue = "InitialValue";
|
||||
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
private const string AttributeNamespace = "System.Windows.Extensions";
|
||||
private const string AttributeName = "GenerateDependencyPropertyAttribute";
|
||||
private const string DependencyPropertySuffix = "Property";
|
||||
private static readonly ClassTemplate GenerateDependencyPropertyAttribute =
|
||||
new ClassTemplate()
|
||||
.WithUsings(
|
||||
new UsingsTemplate()
|
||||
.WithNamespace("System"))
|
||||
.WithNamespace(
|
||||
new NamespaceTemplate()
|
||||
.WithNamespace(AttributeNamespace))
|
||||
.WithConstructor(
|
||||
new ConstructorTemplate()
|
||||
.WithType(AttributeName)
|
||||
.WithModifiers(Modifier.Public))
|
||||
.WithAttributes(
|
||||
new AttributeTemplate()
|
||||
.WithAttribute("AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)"))
|
||||
.WithModifiers(Modifier.Public, Modifier.Sealed)
|
||||
.WithName(AttributeName)
|
||||
.WithBase("Attribute")
|
||||
.WithProperty(
|
||||
new PropertyTemplate()
|
||||
.WithModifiers(Modifier.Public)
|
||||
.WithName("InitialValue")
|
||||
.WithType("object"));
|
||||
|
||||
public void Initialize(GeneratorInitializationContext context)
|
||||
context.RegisterPostInitializationOutput(context =>
|
||||
{
|
||||
// Register a syntax receiver that will be created for each generation pass
|
||||
context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
|
||||
var attributeNamespaceBuilder = SyntaxBuilder.CreateNamespace(AttributeNamespace)
|
||||
.WithUsing("System.ComponentModel")
|
||||
.WithClass(SyntaxBuilder.CreateClass(AttributeName)
|
||||
.WithModifier(Public)
|
||||
.WithConstructor(SyntaxBuilder.CreateConstructor(AttributeName)
|
||||
.WithModifier(Public))
|
||||
.WithAttribute(SyntaxBuilder.CreateAttribute("AttributeUsage")
|
||||
.WithArgument(AttributeTargets.Field)
|
||||
.WithArgument("Inherited", false)
|
||||
.WithArgument("AllowMultiple", false))
|
||||
.WithBaseClass(nameof(Attribute))
|
||||
.WithProperty(SyntaxBuilder.CreateProperty("object", InitialValue)
|
||||
.WithModifier(Public)
|
||||
.WithAccessor(SyntaxBuilder.CreateGetter())
|
||||
.WithAccessor(SyntaxBuilder.CreateSetter())));
|
||||
var attributeSyntax = attributeNamespaceBuilder.Build();
|
||||
var source = attributeSyntax.ToFullString();
|
||||
context.AddSource($"{AttributeName}.g", source);
|
||||
});
|
||||
|
||||
var classDeclarations = context.SyntaxProvider.CreateSyntaxProvider(
|
||||
predicate: static (s, _) => s is FieldDeclarationSyntax,
|
||||
transform: static (ctx, _) => GetFilteredFieldDeclarationSyntax(ctx)).Where(static c => c is not null);
|
||||
|
||||
var compilationAndClasses = context.CompilationProvider.Combine(classDeclarations.Collect());
|
||||
context.RegisterSourceOutput(compilationAndClasses, (sourceProductionContext, tuple) => Execute(tuple.Left, tuple.Right, sourceProductionContext));
|
||||
}
|
||||
|
||||
private static ClassDeclarationSyntax? GetFilteredFieldDeclarationSyntax(GeneratorSyntaxContext context)
|
||||
{
|
||||
var fieldDeclarationSyntax = (FieldDeclarationSyntax)context.Node;
|
||||
if (fieldDeclarationSyntax.AttributeLists
|
||||
.SelectMany(l => l.Attributes)
|
||||
.OfType<AttributeSyntax>()
|
||||
.Any(s => s.Name.ToString() == AttributeName || s.Name.ToString() == AttributeShortName))
|
||||
{
|
||||
return fieldDeclarationSyntax.Parent as ClassDeclarationSyntax;
|
||||
}
|
||||
|
||||
public void Execute(GeneratorExecutionContext context)
|
||||
return default;
|
||||
}
|
||||
|
||||
private static void Execute(Compilation _, ImmutableArray<ClassDeclarationSyntax?> classes, SourceProductionContext sourceProductionContext)
|
||||
{
|
||||
if (classes.IsDefaultOrEmpty)
|
||||
{
|
||||
var attributeText = GenerateDependencyPropertyAttribute.GenerateString();
|
||||
context.AddSource("GenerateDependencyPropertyAttribute.g", attributeText);
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.SyntaxReceiver is not SyntaxReceiver receiver)
|
||||
return;
|
||||
|
||||
var options = (context.Compilation as CSharpCompilation).SyntaxTrees[0].Options as CSharpParseOptions;
|
||||
var compilation = context.Compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(attributeText, Encoding.UTF8), options));
|
||||
|
||||
var attributeSymbol = compilation.GetTypeByMetadataName($"{AttributeNamespace}.{AttributeName}");
|
||||
if (attributeSymbol is null)
|
||||
var distinctClasses = classes.Distinct();
|
||||
foreach (var c in distinctClasses.OfType<ClassDeclarationSyntax>())
|
||||
{
|
||||
var className = c.Identifier.ToFullString().Trim();
|
||||
if (!c.Modifiers.Any(m => m.ToString() == Partial))
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostics.MissingAttributeDiagnostic(AttributeName, AttributeNamespace));
|
||||
return;
|
||||
Diagnostics.ClassNotPartial(className);
|
||||
continue;
|
||||
}
|
||||
|
||||
var fieldSymbols = new List<IFieldSymbol>();
|
||||
foreach (var field in receiver.CandidateFields)
|
||||
var originalNamespaceSyntax = GetParentOfType<BaseNamespaceDeclarationSyntax>(c);
|
||||
if (originalNamespaceSyntax is null)
|
||||
{
|
||||
var model = compilation.GetSemanticModel(field.SyntaxTree);
|
||||
foreach (var variable in field.Declaration.Variables)
|
||||
Diagnostics.ClassNotInNamespace(className);
|
||||
continue;
|
||||
}
|
||||
|
||||
var generatedClassBuilder = SyntaxBuilder.CreateClass(className)
|
||||
.WithModifiers(string.Join(" ", c.Modifiers.Select(m => m.ToFullString())));
|
||||
var generatedNamespaceBuilder = SyntaxBuilder.CreateNamespace(originalNamespaceSyntax.Name.ToFullString())
|
||||
.WithClass(generatedClassBuilder);
|
||||
|
||||
if (originalNamespaceSyntax.Usings.OfType<UsingDirectiveSyntax>().Any(u => u.Name?.ToString() == DependencyPropertyNamespace) is false &&
|
||||
(originalNamespaceSyntax.Parent as CompilationUnitSyntax)?.Usings.Any(u => u.Name?.ToString() == DependencyPropertyNamespace) is false)
|
||||
{
|
||||
generatedNamespaceBuilder.WithUsing(DependencyPropertyNamespace);
|
||||
}
|
||||
|
||||
foreach (var field in c.DescendantNodes().OfType<FieldDeclarationSyntax>())
|
||||
{
|
||||
var attributeSyntax = field.AttributeLists
|
||||
.SelectMany(l => l.Attributes)
|
||||
.OfType<AttributeSyntax>()
|
||||
.FirstOrDefault(s => s.Name.ToString() == AttributeName ||
|
||||
s.Name.ToString() == AttributeShortName);
|
||||
if (attributeSyntax is null)
|
||||
{
|
||||
var fieldSymbol = model.GetDeclaredSymbol(variable) as IFieldSymbol;
|
||||
if (fieldSymbol.GetAttributes().Any(ad => ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default)))
|
||||
{
|
||||
fieldSymbols.Add(fieldSymbol);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
var initialValueArgument = attributeSyntax.ArgumentList?.Arguments
|
||||
.FirstOrDefault(arg => arg.NameEquals != null && arg.NameEquals.Name.Identifier.ValueText == InitialValue);
|
||||
|
||||
var fieldName = field.Declaration.Variables.First().Identifier.ToFullString().Trim();
|
||||
var fieldType = field.Declaration.Type.ToFullString().Trim();
|
||||
var propertyName = fieldName.TrimStart('_');
|
||||
propertyName = $"{char.ToUpper(propertyName[0])}{propertyName.Substring(1)}".Trim();
|
||||
var dependencyPropertyName = $"{propertyName.Trim()}{DependencyPropertySuffix}".Trim();
|
||||
var propertyBuilder = SyntaxBuilder.CreateProperty(fieldType, propertyName)
|
||||
.WithModifier(Public)
|
||||
.WithAccessor(SyntaxBuilder.CreateGetter()
|
||||
.WithBody($"this.{fieldName} = ({fieldType})this.GetValue({dependencyPropertyName});\r\nreturn this.{fieldName};"))
|
||||
.WithAccessor(SyntaxBuilder.CreateSetter()
|
||||
.WithBody($"this.{fieldName} = value;\r\nthis.SetValue({dependencyPropertyName}, value);"));
|
||||
var dependencyPropertyFieldBuilder = SyntaxBuilder.CreateField(DependencyProperty, dependencyPropertyName)
|
||||
.WithModifiers($"{Public} {Static} {Readonly}")
|
||||
.WithInitializer($"{DependencyProperty}.Register(nameof({propertyName}), typeof({fieldType}), typeof({className}), new PropertyMetadata({initialValueArgument?.Expression.ToString()}))");
|
||||
|
||||
generatedClassBuilder
|
||||
.WithProperty(propertyBuilder)
|
||||
.WithField(dependencyPropertyFieldBuilder);
|
||||
}
|
||||
|
||||
foreach (var group in fieldSymbols.GroupBy(f => f.ContainingType, SymbolEqualityComparer.IncludeNullability))
|
||||
var generatedNamespace = generatedNamespaceBuilder.Build();
|
||||
if (originalNamespaceSyntax.Usings.Count != 0)
|
||||
{
|
||||
var classSource = ProcessClass((INamedTypeSymbol)group.Key, attributeSymbol, group.ToList(), context);
|
||||
context.AddSource($"{group.Key.Name}.DependencyPropertyGenerator.g.cs", classSource);
|
||||
}
|
||||
}
|
||||
|
||||
private static string ProcessClass(INamedTypeSymbol classSymbol, INamedTypeSymbol attributeSymbol, List<IFieldSymbol> fields, GeneratorExecutionContext generatorExecutionContext)
|
||||
{
|
||||
if (!classSymbol.ContainingSymbol.Equals(classSymbol.ContainingNamespace, SymbolEqualityComparer.Default))
|
||||
{
|
||||
var syntaxTree = classSymbol.DeclaringSyntaxReferences.First().SyntaxTree;
|
||||
var textSpan = classSymbol.DeclaringSyntaxReferences.First().Span;
|
||||
generatorExecutionContext.ReportDiagnostic(
|
||||
Diagnostics.ClassNotTopLevelDiagnostic(classSymbol.Name, classSymbol.ContainingSymbol.Name, syntaxTree, textSpan));
|
||||
return null;
|
||||
}
|
||||
|
||||
var namespaceName = classSymbol.ContainingNamespace.ToDisplayString();
|
||||
|
||||
var source = new ClassTemplate()
|
||||
.WithUsings(
|
||||
new UsingsTemplate()
|
||||
.WithNamespace("System"),
|
||||
new UsingsTemplate()
|
||||
.WithNamespace("System.Windows"))
|
||||
.WithNamespace(
|
||||
new NamespaceTemplate()
|
||||
.WithNamespace(namespaceName))
|
||||
.WithName(classSymbol.Name);
|
||||
AssignModifiers(source, classSymbol);
|
||||
foreach (var fieldSymbol in fields)
|
||||
{
|
||||
ProcessField(source, classSymbol, attributeSymbol, fieldSymbol);
|
||||
}
|
||||
|
||||
return source.GenerateString();
|
||||
}
|
||||
|
||||
private static void AssignModifiers(ClassTemplate source, INamedTypeSymbol classSymbol)
|
||||
{
|
||||
//Find class declaration syntax and parse modifiers.
|
||||
if (classSymbol.DeclaringSyntaxReferences.Select(sr => sr.GetSyntax()).OfType<ClassDeclarationSyntax>().First() is ClassDeclarationSyntax classDeclarationSyntax)
|
||||
{
|
||||
source.WithModifiers(classDeclarationSyntax.Modifiers.Select(m => Modifier.Parse(m.ValueText)).ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
private static void ProcessField(ClassTemplate source, INamedTypeSymbol classSymbol, INamedTypeSymbol attributeSymbol, IFieldSymbol fieldSymbol)
|
||||
{
|
||||
var fieldName = fieldSymbol.Name;
|
||||
var fieldType = fieldSymbol.Type;
|
||||
|
||||
string propertyName = GenerateName(fieldName);
|
||||
|
||||
var attributeData = fieldSymbol.GetAttributes().Single(ad => ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default));
|
||||
var initialValueOpt = attributeData.NamedArguments.SingleOrDefault(kvp => kvp.Key == "InitialValue").Value;
|
||||
|
||||
var dependencyPropertyValue = initialValueOpt.Value is null ?
|
||||
@$"DependencyProperty.Register(""{propertyName}"", typeof({fieldType}), typeof({classSymbol.Name}))" :
|
||||
@$"DependencyProperty.Register(""{propertyName}"", typeof({fieldType}), typeof({classSymbol.Name}), new PropertyMetadata({initialValueOpt.ToCSharpString()}))";
|
||||
|
||||
source
|
||||
.WithField(
|
||||
new FieldTemplate()
|
||||
.WithModifiers(Modifier.Public, Modifier.Static, Modifier.Readonly)
|
||||
.WithType("DependencyProperty")
|
||||
.WithName(propertyName + DependencyPropertySuffix)
|
||||
.WithValue(dependencyPropertyValue))
|
||||
.WithProperty(
|
||||
new PropertyTemplate()
|
||||
.WithType(fieldType.ToString())
|
||||
.WithName(propertyName)
|
||||
.WithGetter(
|
||||
new GetterTemplate()
|
||||
.WithCode(
|
||||
new CodeBlockTemplate()
|
||||
.WithLine($"this.{fieldName} = ({fieldType})this.GetValue({propertyName + DependencyPropertySuffix});")
|
||||
.WithLine($"return ({fieldType})this.GetValue({propertyName + DependencyPropertySuffix});")))
|
||||
.WithSetter(
|
||||
new SetterTemplate()
|
||||
.WithCode(
|
||||
new CodeBlockTemplate()
|
||||
.WithLine($"this.{fieldName} = value;")
|
||||
.WithLine($"this.SetValue({propertyName + DependencyPropertySuffix}, value);"))));
|
||||
}
|
||||
|
||||
private static string GenerateName(string fieldName)
|
||||
{
|
||||
fieldName = fieldName.TrimStart('_');
|
||||
if (char.IsUpper(fieldName.First()))
|
||||
{
|
||||
return char.ToLower(fieldName.First()) + fieldName.Substring(1);
|
||||
generatedNamespace = generatedNamespace.AddUsings([.. originalNamespaceSyntax.Usings]);
|
||||
}
|
||||
else
|
||||
{
|
||||
return char.ToUpper(fieldName.First()) + fieldName.Substring(1);
|
||||
}
|
||||
}
|
||||
|
||||
internal class SyntaxReceiver : ISyntaxReceiver
|
||||
{
|
||||
public List<FieldDeclarationSyntax> CandidateFields { get; } = new List<FieldDeclarationSyntax>();
|
||||
|
||||
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
|
||||
{
|
||||
if (syntaxNode is FieldDeclarationSyntax fieldDeclarationSyntax &&
|
||||
fieldDeclarationSyntax.AttributeLists.Count > 0)
|
||||
if (originalNamespaceSyntax.Parent is CompilationUnitSyntax compilationUnit)
|
||||
{
|
||||
this.CandidateFields.Add(fieldDeclarationSyntax);
|
||||
generatedNamespace = generatedNamespace.AddUsings([.. compilationUnit.Usings]);
|
||||
}
|
||||
}
|
||||
|
||||
var fileSource = generatedNamespace.ToFullString();
|
||||
sourceProductionContext.AddSource($"{className}.g", fileSource);
|
||||
}
|
||||
}
|
||||
|
||||
private static T? GetParentOfType<T>(SyntaxNode syntaxNode)
|
||||
{
|
||||
if (syntaxNode.Parent is null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
if (syntaxNode.Parent is T parentNode)
|
||||
{
|
||||
return parentNode;
|
||||
}
|
||||
|
||||
return GetParentOfType<T>(syntaxNode.Parent);
|
||||
}
|
||||
}
|
||||
#nullable disable
|
||||
Reference in New Issue
Block a user