Introduce source generators package

Update dependencies
This commit is contained in:
2024-08-28 15:24:18 +02:00
parent 00459fc59b
commit 552b72b1b9
9 changed files with 307 additions and 11 deletions
+6
View File
@@ -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
@@ -8,9 +8,9 @@
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.3.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.3.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.5.2" />
<PackageReference Include="MSTest.TestFramework" Version="3.5.2" />
<PackageReference Include="coverlet.collector" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -6,7 +6,7 @@
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<LangVersion>latest</LangVersion>
<Version>1.4.0</Version>
<Version>1.4.1</Version>
<Authors>Alexandru Macocian</Authors>
<RepositoryUrl>https://github.com/AlexMacocian/SystemExtensions</RepositoryUrl>
<Description>Extensions for the Slim Dependency Injection engine</Description>
@@ -23,7 +23,7 @@
<PackageReference Include="Microsoft.CorrelationVector" Version="1.0.42" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Slim" Version="1.9.2" />
<PackageReference Include="SystemExtensions.NetStandard" Version="1.6.3" />
<PackageReference Include="SystemExtensions.NetStandard" Version="1.6.6" />
</ItemGroup>
</Project>
@@ -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);
}
}
@@ -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<AttributeSyntax>()
.Any(s => s.Name.ToString() == AttributeName || s.Name.ToString() == AttributeShortName))
{
return fieldDeclarationSyntax.Parent as ClassDeclarationSyntax;
}
return default;
}
private static void Execute(Compilation compilation, ImmutableArray<ClassDeclarationSyntax?> 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<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 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<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 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<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
@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<Authors>Alexandru Macocian</Authors>
<Description>Source generators extensions for netstandard2.0.</Description>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<Version>0.1.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.4" PrivateAssets="all">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" PrivateAssets="all" />
<PackageReference Include="Sybil" Version="0.6.0" PrivateAssets="all" />
</ItemGroup>
<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>
@@ -8,9 +8,9 @@
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.3.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.3.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.5.2" />
<PackageReference Include="MSTest.TestFramework" Version="3.5.2" />
<PackageReference Include="coverlet.collector" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -8,9 +8,9 @@
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.3.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.3.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.5.2" />
<PackageReference Include="MSTest.TestFramework" Version="3.5.2" />
<PackageReference Include="coverlet.collector" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+6
View File
@@ -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