diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml
index 8a6bf39..cc99fec 100644
--- a/.github/workflows/cd.yaml
+++ b/.github/workflows/cd.yaml
@@ -55,6 +55,9 @@ jobs:
- name: Build SystemExtensions.NetStandard.Security project
run: dotnet build SystemExtensions.NetStandard.Security -c $env:Configuration
+ - name: Build SystemExtensions.NetStandard.Generators project
+ run: dotnet build SystemExtensions.NetStandard.Generators -c $env:Configuration
+
- name: Package SystemExtensions.NetStandard
run: dotnet pack -c Release -o . SystemExtensions.NetStandard\SystemExtensions.NetStandard.csproj
@@ -67,5 +70,8 @@ jobs:
- name: Package SystemExtensions.NetStandard.Security
run: dotnet pack -c Release -o . SystemExtensions.NetStandard.Security\SystemExtensions.NetStandard.Security.csproj
+ - name: Package SystemExtensions.NetStandard.Generators
+ run: dotnet pack -c Release -o . SystemExtensions.NetStandard.Generators\SystemExtensions.NetStandard.Generators.csproj
+
- name: Publish
run: dotnet nuget push *.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
\ No newline at end of file
diff --git a/SystemExtensions.DependencyInjection.Tests/SystemExtensions.NetStandard.DependencyInjection.Tests.csproj b/SystemExtensions.DependencyInjection.Tests/SystemExtensions.NetStandard.DependencyInjection.Tests.csproj
index cd8dc0f..2cdb066 100644
--- a/SystemExtensions.DependencyInjection.Tests/SystemExtensions.NetStandard.DependencyInjection.Tests.csproj
+++ b/SystemExtensions.DependencyInjection.Tests/SystemExtensions.NetStandard.DependencyInjection.Tests.csproj
@@ -8,9 +8,9 @@
-
-
-
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/SystemExtensions.NetStandard.DependencyInjection/SystemExtensions.NetStandard.DependencyInjection.csproj b/SystemExtensions.NetStandard.DependencyInjection/SystemExtensions.NetStandard.DependencyInjection.csproj
index 4c9eef2..22e1998 100644
--- a/SystemExtensions.NetStandard.DependencyInjection/SystemExtensions.NetStandard.DependencyInjection.csproj
+++ b/SystemExtensions.NetStandard.DependencyInjection/SystemExtensions.NetStandard.DependencyInjection.csproj
@@ -6,7 +6,7 @@
LICENSE
true
latest
- 1.4.0
+ 1.4.1
Alexandru Macocian
https://github.com/AlexMacocian/SystemExtensions
Extensions for the Slim Dependency Injection engine
@@ -23,7 +23,7 @@
-
+
diff --git a/SystemExtensions.NetStandard.Generators/Diagnostics.cs b/SystemExtensions.NetStandard.Generators/Diagnostics.cs
new file mode 100644
index 0000000..5eb452f
--- /dev/null
+++ b/SystemExtensions.NetStandard.Generators/Diagnostics.cs
@@ -0,0 +1,79 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Text;
+
+namespace System.Extensions
+{
+ internal static class Diagnostics
+ {
+ 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(
+ DependencyPropertyGenerator_NoAttributeFound,
+ $"{attributeNamespace}.{attributeName} not found",
+ "Could not find attribute with name {0} in namespace {1}.",
+ "WpfExtended.SourceGeneration",
+ DiagnosticSeverity.Error,
+ true,
+ $"This error occurs when the attribute generated by the {nameof(DependencyPropertyGenerator)} is not found",
+ null),
+ Location.None,
+ attributeName, attributeNamespace);
+
+ public static Diagnostic ClassNotTopLevelDiagnostic(string className, string containingSymbol, SyntaxTree syntaxTree, TextSpan textSpan) => Diagnostic.Create(
+ new DiagnosticDescriptor(
+ DependencyPropertyGenerator_ClassMustBeTopLevel,
+ $"{className} must be top level",
+ "Class {0} must be top level. It is currently declared under {1}.",
+ "WpfExtended.SourceGeneration",
+ DiagnosticSeverity.Error,
+ true,
+ null,
+ 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);
+ }
+}
diff --git a/SystemExtensions.NetStandard.Generators/NotifyPropertyChangedGenerator.cs b/SystemExtensions.NetStandard.Generators/NotifyPropertyChangedGenerator.cs
new file mode 100644
index 0000000..0a0fa82
--- /dev/null
+++ b/SystemExtensions.NetStandard.Generators/NotifyPropertyChangedGenerator.cs
@@ -0,0 +1,173 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Sybil;
+using System.Collections.Immutable;
+using System.Linq;
+
+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 compilationUnitBuilder = SyntaxBuilder.CreateCompilationUnit()
+ .WithNamespace(
+ SyntaxBuilder.CreateNamespace(AttributeNamespace)
+ .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 compilationUnitSyntax = compilationUnitBuilder.Build();
+ var source = compilationUnitSyntax.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()
+ .Any(s => s.Name.ToString() == AttributeName || s.Name.ToString() == AttributeShortName))
+ {
+ return fieldDeclarationSyntax.Parent as ClassDeclarationSyntax;
+ }
+
+ return default;
+ }
+
+ private static void Execute(Compilation compilation, ImmutableArray classes, SourceProductionContext sourceProductionContext)
+ {
+ if (classes.IsDefaultOrEmpty)
+ {
+ return;
+ }
+
+ var maybeLanguageVersion = (compilation.SyntaxTrees.FirstOrDefault()?.Options as CSharpParseOptions)?.LanguageVersion;
+ if (!maybeLanguageVersion.HasValue)
+ {
+ return;
+ }
+
+ var languageVersion = maybeLanguageVersion.Value;
+ var distinctClasses = classes.Distinct();
+ foreach (var c in distinctClasses.OfType())
+ {
+ var className = c.Identifier.ToFullString();
+ if (!c.Modifiers.Any(m => m.ToString() == Partial))
+ {
+ Diagnostics.ClassNotPartial(className);
+ continue;
+ }
+
+ var originalNamespaceSyntax = GetParentOfType(c);
+ if (originalNamespaceSyntax is null)
+ {
+ Diagnostics.ClassNotInNamespace(className);
+ continue;
+ }
+
+ if (!c.Members
+ .OfType()
+ .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 compilationUnitBuilder = SyntaxBuilder.CreateCompilationUnit()
+ .WithNamespace(
+ (languageVersion >= LanguageVersion.CSharp10 ?
+ SyntaxBuilder.CreateFileScopedNamespace(originalNamespaceSyntax.Name.ToFullString()) :
+ SyntaxBuilder.CreateNamespace(originalNamespaceSyntax.Name.ToFullString()))
+ .WithClass(generatedClassBuilder));
+
+ foreach (var field in c.DescendantNodes().OfType())
+ {
+ if (field.AttributeLists
+ .SelectMany(l => l.Attributes)
+ .OfType()
+ .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 generatedSyntax = compilationUnitBuilder.Build();
+ if (originalNamespaceSyntax.Usings.Count != 0)
+ {
+ generatedSyntax = generatedSyntax.AddUsings([.. originalNamespaceSyntax.Usings]);
+ }
+ else
+ {
+ if (originalNamespaceSyntax.Parent is CompilationUnitSyntax compilationUnit)
+ {
+ generatedSyntax = generatedSyntax.AddUsings([.. compilationUnit.Usings]);
+ }
+ }
+
+ var fileSource = generatedSyntax.ToFullString();
+ sourceProductionContext.AddSource($"{className}.g", fileSource);
+ }
+ }
+
+ private static T? GetParentOfType(SyntaxNode syntaxNode)
+ {
+ if (syntaxNode.Parent is null)
+ {
+ return default;
+ }
+
+ if (syntaxNode.Parent is T parentNode)
+ {
+ return parentNode;
+ }
+
+ return GetParentOfType(syntaxNode.Parent);
+ }
+}
+#nullable disable
diff --git a/SystemExtensions.NetStandard.Generators/SystemExtensions.NetStandard.Generators.csproj b/SystemExtensions.NetStandard.Generators/SystemExtensions.NetStandard.Generators.csproj
new file mode 100644
index 0000000..4d9e242
--- /dev/null
+++ b/SystemExtensions.NetStandard.Generators/SystemExtensions.NetStandard.Generators.csproj
@@ -0,0 +1,32 @@
+
+
+
+ netstandard2.0
+ true
+ true
+ Alexandru Macocian
+ Source generators extensions for netstandard2.0.
+ LICENSE
+ 0.1.0
+ latest
+ true
+ true
+ true
+ true
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SystemExtensions.NetStandard.Security.Tests/SystemExtensions.NetStandard.Security.Tests.csproj b/SystemExtensions.NetStandard.Security.Tests/SystemExtensions.NetStandard.Security.Tests.csproj
index e4eb01f..d7dd3ef 100644
--- a/SystemExtensions.NetStandard.Security.Tests/SystemExtensions.NetStandard.Security.Tests.csproj
+++ b/SystemExtensions.NetStandard.Security.Tests/SystemExtensions.NetStandard.Security.Tests.csproj
@@ -8,9 +8,9 @@
-
-
-
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/SystemExtensions.Tests/SystemExtensions.NetStandard.Tests.csproj b/SystemExtensions.Tests/SystemExtensions.NetStandard.Tests.csproj
index a3cc103..6e14d26 100644
--- a/SystemExtensions.Tests/SystemExtensions.NetStandard.Tests.csproj
+++ b/SystemExtensions.Tests/SystemExtensions.NetStandard.Tests.csproj
@@ -8,9 +8,9 @@
-
-
-
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/SystemExtensions.sln b/SystemExtensions.sln
index 9419f5c..d55929c 100644
--- a/SystemExtensions.sln
+++ b/SystemExtensions.sln
@@ -33,6 +33,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
.editorconfig = .editorconfig
EndProjectSection
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SystemExtensions.NetStandard.Generators", "SystemExtensions.NetStandard.Generators\SystemExtensions.NetStandard.Generators.csproj", "{BEF74563-A1D8-4159-B332-754A11151173}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -67,6 +69,10 @@ Global
{9E6EBBF0-671B-4941-A72B-2CA60C882038}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9E6EBBF0-671B-4941-A72B-2CA60C882038}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9E6EBBF0-671B-4941-A72B-2CA60C882038}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BEF74563-A1D8-4159-B332-754A11151173}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BEF74563-A1D8-4159-B332-754A11151173}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BEF74563-A1D8-4159-B332-754A11151173}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BEF74563-A1D8-4159-B332-754A11151173}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE