mirror of
https://github.com/AlexMacocian/SystemExtensions.git
synced 2026-07-15 14:19:29 +00:00
Introduce source generators package
Update dependencies
This commit is contained in:
@@ -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
|
||||
+32
@@ -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>
|
||||
Reference in New Issue
Block a user