Switch to Incremental Generators

Implement INotifyPropertyChanged Generator
This commit is contained in:
2024-04-10 21:48:51 +02:00
parent a4dbcd9d53
commit e6df17358b
25 changed files with 396 additions and 1094 deletions
+33 -16
View File
@@ -1,24 +1,41 @@
using FluentAssertions;
using System.ComponentModel;
using System.Windows.Controls;
using System.Windows.Extensions;
using System.Windows.Media.Effects;
namespace WpfExtended.SourceGeneration.Tests
{
internal partial class Class1 : UserControl
{
[GenerateDependencyProperty]
public int someF;
[GenerateDependencyProperty(InitialValue = "This has a value")]
public string someValue;
[GenerateDependencyProperty]
public Effect effect;
namespace WpfExtended.SourceGeneration.Tests;
public void TestValues()
{
this.SomeF.Should().Be(0);
this.SomeValue.Should().Be("This has a value");
this.Effect.Should().BeNull();
}
internal partial class Class1 : UserControl, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private bool calledEvent;
[GenerateDependencyProperty]
public int someF;
[GenerateDependencyProperty(InitialValue = "This has a value")]
public string someValue;
[GenerateDependencyProperty]
public Effect effect;
[GenerateNotifyPropertyChanged]
public string someNotifyingValue;
public void TestValues()
{
this.PropertyChanged += Class1_PropertyChanged;
this.SomeF.Should().Be(0);
this.SomeValue.Should().Be("This has a value");
this.Effect.Should().BeNull();
this.SomeNotifyingValue = "NewValue";
this.calledEvent.Should().BeTrue();
this.someNotifyingValue.Should().Be("NewValue");
}
private void Class1_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
this.calledEvent = true;
}
}
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework>
<TargetFramework>net8.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<LangVersion>preview</LangVersion>
<OutputType>Exe</OutputType>
-143
View File
@@ -1,143 +0,0 @@
using System.Extensions.Templates;
using System.Linq;
using System.Text;
namespace System.Extensions
{
internal class CodeWriter
{
private StringBuilder StringBuilder { get; } = new StringBuilder();
private CodeBlock CurrentBlock { get; set; } = new CodeBlock();
public CodeWriter Append(string value)
{
this.CurrentBlock.Append(this.StringBuilder, value);
return this;
}
public CodeWriter AppendLine(string value)
{
this.CurrentBlock.AppendLine(this.StringBuilder, value);
return this;
}
public CodeWriter AppendLine()
{
this.CurrentBlock.AppendLine(this.StringBuilder);
return this;
}
public CodeWriter Append(char c)
{
this.CurrentBlock.Append(this.StringBuilder, c);
return this;
}
public CodeWriter AppendLine(char c)
{
this.AppendLine(c);
return this;
}
public CodeWriter Append(AbstractTemplate abstractTemplate)
{
abstractTemplate.Generate(this);
return this;
}
public CodeWriter BeginCodeBlock()
{
this.CurrentBlock.AppendLine(this.StringBuilder).AppendLine(this.StringBuilder, '{');
this.CurrentBlock = CodeBlock.GenerateCodeBlock(this.CurrentBlock);
return this;
}
public CodeWriter EndCodeBlock()
{
this.CurrentBlock = this.CurrentBlock.End(this.StringBuilder).Parent;
this.CurrentBlock.AppendLine(this.StringBuilder, '}');
return this;
}
public override string ToString()
{
return this.StringBuilder.ToString();
}
private class CodeBlock
{
private const char PrefixSymbol = '\t';
private bool NewLine { get; set; } = true;
private string Prefix { get; }
public CodeBlock Parent { get; }
public int Level { get; }
public CodeBlock()
{
this.Level = 0;
this.Prefix = string.Empty;
}
private CodeBlock(int level, CodeBlock parent)
{
this.Level = level;
this.Prefix = new string(Enumerable.Repeat(PrefixSymbol, this.Level).ToArray());
this.Parent = parent;
}
public CodeBlock AppendLine(StringBuilder stringBuilder)
{
stringBuilder.AppendLine();
this.NewLine = true;
return this;
}
public CodeBlock AppendLine(StringBuilder stringBuilder, string value)
{
value = this.NewLine is true ? Prefix + value : value;
stringBuilder.AppendLine(value);
this.NewLine = true;
return this;
}
public CodeBlock Append(StringBuilder stringBuilder, string value)
{
value = this.NewLine is true ? Prefix + value : value;
stringBuilder.Append(value);
this.NewLine = false;
return this;
}
public CodeBlock Append(StringBuilder stringBuilder, char c)
{
var value = this.NewLine is true ? Prefix + c : c.ToString();
stringBuilder.Append(value);
this.NewLine = false;
return this;
}
public CodeBlock AppendLine(StringBuilder stringBuilder, char c)
{
var value = this.NewLine is true ? Prefix + c : c.ToString();
stringBuilder.AppendLine(value);
this.NewLine = true;
return this;
}
public CodeBlock End(StringBuilder stringBuilder)
{
if (this.NewLine is false)
{
stringBuilder.AppendLine();
this.NewLine = true;
}
return this;
}
public static CodeBlock GenerateCodeBlock(CodeBlock codeBlock)
{
var child = new CodeBlock(codeBlock.Level + 1, codeBlock);
return child;
}
}
}
}
@@ -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
@@ -7,6 +7,9 @@ namespace System.Extensions
{
public const string DependencyPropertyGenerator_NoAttributeFound = "DGD 0001";
public const string DependencyPropertyGenerator_ClassMustBeTopLevel = "DGD 1001";
public const string DependencyPropertyGenerator_ClassMustBePartial = "DGD 1002";
public const string DependencyPropertyGenerator_ClassMustBeInNamespace = "DGD 1003";
public const string DependencyPropertyGenerator_ClassMustImplementINotifyPropertyChanged = "DGD 1004";
public static Diagnostic MissingAttributeDiagnostic(string attributeName, string attributeNamespace) => Diagnostic.Create(
new DiagnosticDescriptor(
@@ -33,5 +36,44 @@ namespace System.Extensions
null),
Location.Create(syntaxTree, textSpan),
className, containingSymbol);
public static Diagnostic ClassNotPartial(string className) => Diagnostic.Create(
new DiagnosticDescriptor(
DependencyPropertyGenerator_ClassMustBePartial,
$"{className} is not partial",
"Class {0} must be marked as partial in order for the generator to work",
"WpfExtended.SourceGeneration",
DiagnosticSeverity.Error,
true,
null,
null),
Location.None,
className);
public static Diagnostic ClassNotInNamespace(string className) => Diagnostic.Create(
new DiagnosticDescriptor(
DependencyPropertyGenerator_ClassMustBeInNamespace,
$"{className} is not defined in a namespace",
"Class {0} must be defined inside a namespace for the generator to work",
"WpfExtended.SourceGeneration",
DiagnosticSeverity.Error,
true,
null,
null),
Location.None,
className);
public static Diagnostic ClassDoesNotImplementINotifyPropertyChanged(string className) => Diagnostic.Create(
new DiagnosticDescriptor(
DependencyPropertyGenerator_ClassMustImplementINotifyPropertyChanged,
$"{className} must implement INotifyPropertyChanged",
"Class {0} must implement INotifyPropertyChanged for the generator to work",
"WpfExtended.SourceGeneration",
DiagnosticSeverity.Error,
true,
null,
null),
Location.None,
className);
}
}
@@ -0,0 +1,164 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Sybil;
using System.Linq;
using System.Collections.Immutable;
namespace System.Extensions;
#nullable enable
[Generator(LanguageNames.CSharp)]
public class NotifyPropertyChangedGenerator : IIncrementalGenerator
{
private const string AttributeNamespace = "System.Windows.Extensions";
private const string AttributeName = "GenerateNotifyPropertyChangedAttribute";
private const string AttributeShortName = "GenerateNotifyPropertyChanged";
private const string PropertyChangedEventHandler = "PropertyChangedEventHandler";
private const string NullablePropertyChangedEventHandler = "PropertyChangedEventHandler?";
private const string PropertyChanged = "PropertyChanged";
private const string Partial = "partial";
private const string Public = "public";
public void Initialize(IncrementalGeneratorInitializationContext context)
{
context.RegisterPostInitializationOutput(context =>
{
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)));
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;
}
return default;
}
private static void Execute(Compilation _, ImmutableArray<ClassDeclarationSyntax?> classes, SourceProductionContext sourceProductionContext)
{
if (classes.IsDefaultOrEmpty)
{
return;
}
var distinctClasses = classes.Distinct();
foreach (var c in distinctClasses.OfType<ClassDeclarationSyntax>())
{
var className = c.Identifier.ToFullString();
if (!c.Modifiers.Any(m => m.ToString() == Partial))
{
Diagnostics.ClassNotPartial(className);
continue;
}
var originalNamespaceSyntax = GetParentOfType<BaseNamespaceDeclarationSyntax>(c);
if (originalNamespaceSyntax is null)
{
Diagnostics.ClassNotInNamespace(className);
continue;
}
if (!c.Members
.OfType<EventFieldDeclarationSyntax>()
.Any(eventField =>
(eventField.Declaration.Variables.Any(variable => variable.Identifier.Text == PropertyChanged) &&
eventField.Modifiers.Any(modifier => modifier.Text == Public) &&
eventField.Declaration.Type.ToString() == PropertyChangedEventHandler) ||
eventField.Declaration.Type.ToString() == NullablePropertyChangedEventHandler))
{
Diagnostics.ClassDoesNotImplementINotifyPropertyChanged(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.Count != 0)
{
foreach (var u in originalNamespaceSyntax.Usings.OfType<UsingDirectiveSyntax>())
{
generatedNamespaceBuilder.WithUsing(u.Name?.ToString());
}
}
else
{
if (originalNamespaceSyntax.Parent is CompilationUnitSyntax compilationUnit)
{
foreach(var u in compilationUnit.Usings.OfType<UsingDirectiveSyntax>())
{
generatedNamespaceBuilder.WithUsing(u.Name?.ToString());
}
}
}
foreach (var field in c.DescendantNodes().OfType<FieldDeclarationSyntax>())
{
if (field.AttributeLists
.SelectMany(l => l.Attributes)
.OfType<AttributeSyntax>()
.Any(s => s.Name.ToString() == AttributeName ||
s.Name.ToString() == AttributeShortName) is false)
{
continue;
}
var fieldName = field.Declaration.Variables.First().Identifier.ToFullString();
var propertyName = fieldName.TrimStart('_');
propertyName = $"{char.ToUpper(propertyName[0])}{propertyName.Substring(1)}";
var propertyBuilder = SyntaxBuilder.CreateProperty(field.Declaration.Type.ToFullString(), propertyName)
.WithModifier(Public)
.WithAccessor(SyntaxBuilder.CreateGetter()
.WithBody($"return this.{fieldName};"))
.WithAccessor(SyntaxBuilder.CreateSetter()
.WithBody($"this.{fieldName} = value;\r\nthis.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof({propertyName})));"));
generatedClassBuilder.WithProperty(propertyBuilder);
}
var fileSource = generatedNamespaceBuilder.Build().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
@@ -1,7 +0,0 @@
namespace System.Extensions.Templates
{
internal abstract class AbstractTemplate
{
public abstract void Generate(CodeWriter codeWriter);
}
}
@@ -1,28 +0,0 @@
namespace System.Extensions.Templates
{
internal sealed class ArgumentTemplate : AbstractTemplate
{
public string Name { get; set; }
public string Type { get; set; }
public ArgumentTemplate WithName(string name)
{
this.Name = name;
return this;
}
public ArgumentTemplate WithType(string type)
{
this.Type = type;
return this;
}
public override void Generate(CodeWriter codeWriter)
{
codeWriter
.Append(this.Type)
.Append(' ')
.Append(this.Name);
}
}
}
@@ -1,21 +0,0 @@
namespace System.Extensions.Templates
{
internal sealed class AttributeTemplate : AbstractTemplate
{
public string Attribute { get; set; }
public AttributeTemplate WithAttribute(string attribute)
{
this.Attribute = attribute;
return this;
}
public override void Generate(CodeWriter codeWriter)
{
codeWriter
.Append('[')
.Append(this.Attribute)
.Append(']');
}
}
}
@@ -1,190 +0,0 @@
using System.Collections.Generic;
namespace System.Extensions.Templates
{
internal sealed class ClassTemplate : AbstractTemplate
{
public List<AttributeTemplate> Attributes { get; } = new List<AttributeTemplate>();
public List<UsingsTemplate> Usings { get; } = new List<UsingsTemplate>();
public List<Modifier> Modifiers { get; } = new List<Modifier>();
public List<PropertyTemplate> Properties { get; } = new List<PropertyTemplate>();
public List<FieldTemplate> Fields { get; } = new List<FieldTemplate>();
public List<MethodTemplate> Methods { get; } = new List<MethodTemplate>();
public List<ConstructorTemplate> Constructors { get; } = new List<ConstructorTemplate>();
public NamespaceTemplate Namespace { get; set; }
public string Name { get; set; }
public string Base { get; set; }
public ClassTemplate WithAttributes(params AttributeTemplate[] attributeTemplates)
{
this.Attributes.Clear();
this.Attributes.AddRange(attributeTemplates);
return this;
}
public ClassTemplate WithAttribute(AttributeTemplate attributeTemplate)
{
this.Attributes.Add(attributeTemplate);
return this;
}
public ClassTemplate WithUsings(params UsingsTemplate[] usingsTemplates)
{
this.Usings.Clear();
this.Usings.AddRange(usingsTemplates);
return this;
}
public ClassTemplate WithUsing(UsingsTemplate usingsTemplate)
{
this.Usings.Add(usingsTemplate);
return this;
}
public ClassTemplate WithModifiers(params Modifier[] modifiers)
{
this.Modifiers.Clear();
this.Modifiers.AddRange(modifiers);
return this;
}
public ClassTemplate WithModifier(Modifier modifier)
{
this.Modifiers.Add(modifier);
return this;
}
public ClassTemplate WithMethods(params MethodTemplate[] methodTemplates)
{
this.Methods.Clear();
this.Methods.AddRange(methodTemplates);
return this;
}
public ClassTemplate WithMethod(MethodTemplate methodTemplate)
{
this.Methods.Add(methodTemplate);
return this;
}
public ClassTemplate WithFields(params FieldTemplate[] fieldTemplates)
{
this.Fields.Clear();
this.Fields.AddRange(fieldTemplates);
return this;
}
public ClassTemplate WithField(FieldTemplate fieldTemplate)
{
this.Fields.Add(fieldTemplate);
return this;
}
public ClassTemplate WithProperties(params PropertyTemplate[] propertyTemplates)
{
this.Properties.Clear();
this.Properties.AddRange(propertyTemplates);
return this;
}
public ClassTemplate WithProperty(PropertyTemplate propertyTemplate)
{
this.Properties.Add(propertyTemplate);
return this;
}
public ClassTemplate WithConstructors(params ConstructorTemplate[] constructorTemplates)
{
this.Constructors.Clear();
this.Constructors.AddRange(constructorTemplates);
return this;
}
public ClassTemplate WithConstructor(ConstructorTemplate constructorTemplate)
{
this.Constructors.Add(constructorTemplate);
return this;
}
public ClassTemplate WithNamespace(NamespaceTemplate namespaceTemplate)
{
this.Namespace = namespaceTemplate;
return this;
}
public ClassTemplate WithName(string name)
{
this.Name = name;
return this;
}
public ClassTemplate WithBase(string b)
{
this.Base = b;
return this;
}
public override void Generate(CodeWriter codeWriter)
{
foreach(var u in this.Usings)
{
codeWriter
.Append(u)
.AppendLine();
}
codeWriter
.AppendLine()
.Append(this.Namespace)
.BeginCodeBlock();
foreach(var attribute in this.Attributes)
{
codeWriter
.Append(attribute)
.AppendLine();
}
foreach(var modifier in this.Modifiers)
{
codeWriter
.Append(modifier)
.Append(' ');
}
codeWriter
.Append("class")
.Append(' ')
.Append(this.Name);
if (!string.IsNullOrEmpty(this.Base))
{
codeWriter
.Append(' ')
.Append(':')
.Append(' ')
.Append(this.Base);
}
codeWriter.BeginCodeBlock();
foreach(var field in this.Fields)
{
codeWriter
.Append(field)
.AppendLine();
}
foreach(var property in this.Properties)
{
codeWriter
.Append(property)
.AppendLine();
}
foreach(var constructor in this.Constructors)
{
codeWriter
.Append(constructor)
.AppendLine();
}
foreach(var method in this.Methods)
{
codeWriter
.Append(method)
.AppendLine();
}
codeWriter.EndCodeBlock().EndCodeBlock();
}
public string GenerateString()
{
var codeWriter = new CodeWriter();
this.Generate(codeWriter);
return codeWriter.ToString();
}
}
}
@@ -1,30 +0,0 @@
using System.Collections.Generic;
namespace System.Extensions.Templates
{
internal sealed class CodeBlockTemplate : CodeTemplate
{
public List<string> Lines { get; set; } = new List<string>();
public CodeBlockTemplate WithLines(params string[] lines)
{
this.Lines.Clear();
this.Lines.AddRange(lines);
return this;
}
public CodeBlockTemplate WithLine(string line)
{
this.Lines.Add(line);
return this;
}
public override void Generate(CodeWriter codeWriter)
{
foreach(var line in this.Lines)
{
codeWriter.AppendLine(line);
}
}
}
}
@@ -1,6 +0,0 @@
namespace System.Extensions.Templates
{
internal abstract class CodeTemplate : AbstractTemplate
{
}
}
@@ -1,82 +0,0 @@
using System.Collections.Generic;
namespace System.Extensions.Templates
{
internal sealed class ConstructorTemplate : AbstractTemplate
{
public List<Modifier> Modifiers { get; } = new List<Modifier>();
public List<ArgumentTemplate> Arguments { get; } = new List<ArgumentTemplate>();
public string Type { get; set; }
public string Body { get; set; }
public string Base { get; set; }
public ConstructorTemplate WithType(string type)
{
this.Type = type;
return this;
}
public ConstructorTemplate WithBody(string body)
{
this.Body = body;
return this;
}
public ConstructorTemplate WithBase(string b)
{
this.Base = b;
return this;
}
public ConstructorTemplate WithModifiers(params Modifier[] modifiers)
{
this.Modifiers.Clear();
this.Modifiers.AddRange(modifiers);
return this;
}
public ConstructorTemplate WithArguments(params ArgumentTemplate[] argumentTemplates)
{
this.Arguments.Clear();
this.Arguments.AddRange(argumentTemplates);
return this;
}
public ConstructorTemplate WithArgument(ArgumentTemplate argumentTemplate)
{
this.Arguments.Add(argumentTemplate);
return this;
}
public override void Generate(CodeWriter codeWriter)
{
foreach(var modifier in this.Modifiers)
{
codeWriter
.Append(modifier)
.Append(' ');
}
codeWriter
.Append(this.Type)
.Append('(');
for (int i = 0; i < this.Arguments.Count; i++)
{
var argument = this.Arguments[i];
codeWriter.Append(argument);
if (i < this.Arguments.Count - 1)
{
codeWriter.Append(", ");
}
}
codeWriter.Append(')');
if (!string.IsNullOrEmpty(this.Base))
{
codeWriter
.Append(" : base(")
.Append(this.Base)
.Append(')');
}
codeWriter.BeginCodeBlock()
.Append(this.Body)
.EndCodeBlock();
}
}
}
@@ -1,55 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace System.Extensions.Templates
{
internal sealed class FieldTemplate : AbstractTemplate
{
public List<Modifier> Modifiers { get; } = new List<Modifier> { Modifier.Public };
public string Name { get; set; }
public string Value { get; set; }
public string Type { get; set; }
public FieldTemplate WithName(string name)
{
this.Name = name;
return this;
}
public FieldTemplate WithValue(string value)
{
this.Value = value;
return this;
}
public FieldTemplate WithModifiers(params Modifier[] modifiers)
{
this.Modifiers.Clear();
this.Modifiers.AddRange(modifiers);
return this;
}
public FieldTemplate WithType(string type)
{
this.Type = type;
return this;
}
public override void Generate(CodeWriter codeWriter)
{
foreach (var modifier in this.Modifiers)
{
codeWriter
.Append(modifier)
.Append(' ');
}
codeWriter.Append(this.Type)
.Append(' ')
.Append(this.Name)
.Append(' ')
.Append('=')
.Append(' ')
.Append(this.Value)
.Append(';');
}
}
}
@@ -1,48 +0,0 @@
using System.Collections.Generic;
namespace System.Extensions.Templates
{
internal sealed class GetterTemplate : AbstractTemplate
{
public List<Modifier> Modifiers { get; } = new List<Modifier>();
public CodeTemplate Code { get; set; }
public GetterTemplate WithModifiers(params Modifier[] modifiers)
{
this.Modifiers.Clear();
this.Modifiers.AddRange(modifiers);
return this;
}
public GetterTemplate WithCode(CodeTemplate codeTemplate)
{
this.Code = codeTemplate;
return this;
}
public override void Generate(CodeWriter codeWriter)
{
foreach(var modifier in this.Modifiers)
{
codeWriter
.Append(modifier)
.Append(' ');
}
codeWriter.Append("get");
if (this.Code is SimpleCodeTemplate simpleGetter)
{
codeWriter
.Append(simpleGetter)
.AppendLine();
}
else
{
codeWriter
.BeginCodeBlock()
.Append(this.Code)
.EndCodeBlock();
}
}
}
}
@@ -1,72 +0,0 @@
using System.Collections.Generic;
namespace System.Extensions.Templates
{
internal sealed class MethodTemplate : AbstractTemplate
{
public List<Modifier> Modifiers { get; } = new List<Modifier>();
public List<ArgumentTemplate> Arguments { get; } = new List<ArgumentTemplate>();
public string Name { get; set; }
public string ReturnType { get; set; }
public string Body { get; set; }
public MethodTemplate WithModifiers(params Modifier[] modifiers)
{
this.Modifiers.Clear();
this.Modifiers.AddRange(modifiers);
return this;
}
public MethodTemplate WithArguments(params ArgumentTemplate[] argumentTemplates)
{
this.Arguments.Clear();
this.Arguments.AddRange(argumentTemplates);
return this;
}
public MethodTemplate WithName(string name)
{
this.Name = name;
return this;
}
public MethodTemplate WithReturnType(string returnType)
{
this.ReturnType = returnType;
return this;
}
public MethodTemplate WithBody(string body)
{
this.Body = body;
return this;
}
public override void Generate(CodeWriter codeWriter)
{
foreach(var modifier in this.Modifiers)
{
codeWriter
.Append(modifier)
.Append(' ');
}
codeWriter
.Append(this.ReturnType)
.Append(' ')
.Append(this.Name)
.Append('(');
for(int i = 0; i < this.Arguments.Count; i++)
{
var argument = this.Arguments[i];
codeWriter.Append(argument);
if (i < this.Arguments.Count - 1)
{
codeWriter.Append(", ");
}
}
codeWriter
.Append(')')
.BeginCodeBlock()
.Append(this.Body)
.EndCodeBlock();
}
}
}
@@ -1,57 +0,0 @@
using System.Collections;
using System.Collections.Generic;
namespace System.Extensions.Templates
{
internal sealed class Modifier : AbstractTemplate
{
public static Modifier Protected { get; } = new Modifier { Value = "protected" };
public static Modifier Readonly { get; } = new Modifier { Value = "readonly" };
public static Modifier Static { get; } = new Modifier { Value = "static" };
public static Modifier Public { get; } = new Modifier { Value = "public" };
public static Modifier Internal { get; } = new Modifier { Value = "internal" };
public static Modifier Private { get; } = new Modifier { Value = "private" };
public static Modifier Abstract { get; } = new Modifier { Value = "abstract" };
public static Modifier Virtual { get; } = new Modifier { Value = "virtual" };
public static Modifier Partial { get; } = new Modifier { Value = "partial" };
public static Modifier Sealed { get; } = new Modifier { Value = "sealed" };
public string Value { get; private set; }
private Modifier()
{
}
public override void Generate(CodeWriter codeWriter)
{
codeWriter.Append(this.Value);
}
public static bool TryParse(string value, out Modifier modifier)
{
foreach(var supportedModifier in SupportedModifiers)
{
if (supportedModifier.Value.Equals(value, StringComparison.Ordinal))
{
modifier = supportedModifier;
return true;
}
}
modifier = null;
return false;
}
public static Modifier Parse(string value)
{
if (TryParse(value, out var modifier))
{
return modifier;
}
throw new InvalidOperationException($"Could not find a modifier with value {value}");
}
private static IEnumerable<Modifier> SupportedModifiers { get; } = new List<Modifier> { Protected, Readonly, Static, Internal, Public, Private, Abstract, Virtual, Partial, Sealed };
}
}
@@ -1,19 +0,0 @@
namespace System.Extensions.Templates
{
internal sealed class NamespaceTemplate : AbstractTemplate
{
public string Namespace { get; set; }
public NamespaceTemplate WithNamespace(string n)
{
this.Namespace = n;
return this;
}
public override void Generate(CodeWriter codeWriter)
{
codeWriter
.Append("namespace ")
.Append(this.Namespace);
}
}
}
@@ -1,67 +0,0 @@
using System.Collections.Generic;
namespace System.Extensions.Templates
{
internal sealed class PropertyTemplate : AbstractTemplate
{
public static GetterTemplate DefaultGetter = new GetterTemplate()
.WithCode(
new SimpleCodeTemplate()
.WithCode(";"));
public static SetterTemplate DefaultSetter = new SetterTemplate()
.WithCode(
new SimpleCodeTemplate()
.WithCode(";"));
public List<Modifier> Modifiers { get; private set; } = new List<Modifier>() { Modifier.Public };
public GetterTemplate Getter { get; set; } = DefaultGetter;
public SetterTemplate Setter { get; set; } = DefaultSetter;
public string Type { get; set; }
public string Name { get; set; }
public PropertyTemplate WithGetter(GetterTemplate getter)
{
this.Getter = getter;
return this;
}
public PropertyTemplate WithSetter(SetterTemplate setter)
{
this.Setter = setter;
return this;
}
public PropertyTemplate WithModifiers(params Modifier[] modifiers)
{
this.Modifiers.Clear();
this.Modifiers.AddRange(modifiers);
return this;
}
public PropertyTemplate WithName(string name)
{
this.Name = name;
return this;
}
public PropertyTemplate WithType(string type)
{
this.Type = type;
return this;
}
public override void Generate(CodeWriter codeWriter)
{
foreach(var modifier in this.Modifiers)
{
codeWriter
.Append(modifier)
.Append(' ');
}
codeWriter
.Append(this.Type)
.Append(' ')
.Append(this.Name)
.BeginCodeBlock()
.Append(this.Getter)
.Append(this.Setter)
.EndCodeBlock();
}
}
}
@@ -1,48 +0,0 @@
using System.Collections.Generic;
namespace System.Extensions.Templates
{
internal sealed class SetterTemplate : AbstractTemplate
{
public List<Modifier> Modifiers { get; } = new List<Modifier>();
public CodeTemplate Code { get; set; }
public SetterTemplate WithModifiers(params Modifier[] modifiers)
{
this.Modifiers.Clear();
this.Modifiers.AddRange(modifiers);
return this;
}
public SetterTemplate WithCode(CodeTemplate codeTemplate)
{
this.Code = codeTemplate;
return this;
}
public override void Generate(CodeWriter codeWriter)
{
foreach (var modifier in this.Modifiers)
{
codeWriter
.Append(modifier)
.Append(' ');
}
codeWriter.Append("set");
if (this.Code is SimpleCodeTemplate simpleSetter)
{
codeWriter
.Append(simpleSetter)
.AppendLine();
}
else
{
codeWriter
.BeginCodeBlock()
.Append(this.Code)
.EndCodeBlock();
}
}
}
}
@@ -1,18 +0,0 @@
namespace System.Extensions.Templates
{
internal sealed class SimpleCodeTemplate : CodeTemplate
{
public string Code { get; set; }
public SimpleCodeTemplate WithCode(string code)
{
this.Code = code;
return this;
}
public override void Generate(CodeWriter codeWriter)
{
codeWriter.Append(this.Code);
}
}
}
@@ -1,20 +0,0 @@
namespace System.Extensions.Templates
{
internal sealed class UsingsTemplate : AbstractTemplate
{
public string Namespace { get; set; }
public UsingsTemplate WithNamespace(string n)
{
this.Namespace = n;
return this;
}
public override void Generate(CodeWriter codeWriter)
{
codeWriter
.Append("using ")
.Append(this.Namespace)
.Append(';');
}
}
}
@@ -7,16 +7,20 @@
<Authors>Alexandru Macocian</Authors>
<Description>Source generator library.</Description>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<Version>0.1.2</Version>
<Version>0.2.0</Version>
<LangVersion>latest</LangVersion>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<IsRoslynComponent>true</IsRoslynComponent>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all">
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" PrivateAssets="all" />
<PackageReference Include="Sybil" Version="0.5.0" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
@@ -29,6 +33,7 @@
<ItemGroup>
<!-- Package the generator in the analyzer directory of the nuget package -->
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
<None Include="$(OutputPath)\Sybil.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>
</Project>
+1 -1
View File
@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net6.0-windows</TargetFramework>
<TargetFramework>net8.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<StartupObject>WpfExtended.Tests.Launcher</StartupObject>
</PropertyGroup>
+1 -1
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0-windows;netcoreapp3.1</TargetFrameworks>
<TargetFrameworks>net6.0-windows;net7.0-windows;net8.0-windows;netcoreapp3.1</TargetFrameworks>
<UseWPF>true</UseWPF>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>