diff --git a/XAMLTest.Generator/ElementGenerator.cs b/XAMLTest.Generator/ElementGenerator.cs index b7f6279..28e6a72 100644 --- a/XAMLTest.Generator/ElementGenerator.cs +++ b/XAMLTest.Generator/ElementGenerator.cs @@ -1,11 +1,14 @@ -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using TypeInfo = Microsoft.CodeAnalysis.TypeInfo; - -namespace XAMLTest.Generator; - -[Generator] -public class ElementGenerator : ISourceGenerator +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using TypeInfo = Microsoft.CodeAnalysis.TypeInfo; + +namespace XAMLTest.Generator; + +[Generator] +public class ElementGenerator : IIncrementalGenerator { private static DiagnosticDescriptor DuplicateAttributesWarning { get; } = new(id: "XAMLTEST0001", @@ -15,339 +18,363 @@ public class ElementGenerator : ISourceGenerator DiagnosticSeverity.Warning, isEnabledByDefault: true); - public void Execute(GeneratorExecutionContext context) - { - SyntaxReceiver rx = (SyntaxReceiver)context.SyntaxContextReceiver!; - HashSet ignoredTypes = new(); - foreach (var duplicatedAttributes in rx.GeneratedTypes.GroupBy(x => x.Type.FullName).Where(g => g.Count() > 1)) - { - context.ReportDiagnostic(Diagnostic.Create(DuplicateAttributesWarning, Location.None, duplicatedAttributes.Key)); - ignoredTypes.Add(duplicatedAttributes.Key); - return; - } - - foreach (var type in rx.GeneratedTypes) - { - if (ignoredTypes.Contains(type.Type.FullName)) continue; - - if (context.Compilation.GetTypeByMetadataName($"{type.Namespace}.{type.Type.Name}GeneratedExtensions") is not null) - { - continue; - } - - StringBuilder builder = new(); - builder.AppendLine("#nullable enable"); - builder.AppendLine($"namespace {type.Namespace}"); - builder.AppendLine("{"); - builder.AppendLine($" public static partial class {type.Type.Name}GeneratedExtensions"); - builder.AppendLine(" {"); - foreach (var property in type.DependencyProperties) - { - builder.AppendLine(); - if (property.CanRead) - { - builder - .Append(" "); - - if (type.Type.IsFinal) - { - builder.AppendLine($"public static async System.Threading.Tasks.Task<{property.TypeFullName}> Get{property.Name}(this IVisualElement<{type.Type.FullName}> element)"); - } - else - { - builder.AppendLine($"public static async System.Threading.Tasks.Task<{property.TypeFullName}> Get{property.Name}(this IVisualElement element) where T : {type.Type.FullName}"); - } - builder.Append(" ") - .AppendLine($"=> await element.GetProperty<{property.TypeFullName}>(nameof({type.Type.FullName}.{property.Name}));"); - - if (property.TypeFullName.StartsWith("System.Windows.Media.SolidColorBrush") || - property.TypeFullName.StartsWith("System.Windows.Media.Brush")) - { - builder - .Append(" "); - if (type.Type.IsFinal) - { - builder.AppendLine($"public static async System.Threading.Tasks.Task Get{property.Name}Color(this IVisualElement<{type.Type.FullName}> element)"); - } - else - { - builder.AppendLine($"public static async System.Threading.Tasks.Task Get{property.Name}Color(this IVisualElement element) where T : {type.Type.FullName}"); - } - builder - .Append(" ") - .AppendLine($"=> await element.GetProperty(nameof({type.Type.FullName}.{property.Name}));"); - } - } - if (property.CanWrite) - { - builder - .Append(" "); - - if (type.Type.IsFinal) - { - builder.AppendLine($"public static async System.Threading.Tasks.Task<{property.TypeFullName}> Set{property.Name}(this IVisualElement<{type.Type.FullName}> element, {property.TypeFullName} value)"); - } - else - { - builder.AppendLine($"public static async System.Threading.Tasks.Task<{property.TypeFullName}> Set{property.Name}(this IVisualElement element, {property.TypeFullName} value) where T : {type.Type.FullName}"); - } - builder - .Append(" ") - .AppendLine($"=> await element.SetProperty(nameof({type.Type.FullName}.{property.Name}), value);"); - - if (property.TypeFullName.StartsWith("System.Windows.Media.SolidColorBrush") || - property.TypeFullName.StartsWith("System.Windows.Media.Brush")) - { - builder - .Append(" "); - if (type.Type.IsFinal) - { - builder.AppendLine($"public static async System.Threading.Tasks.Task Set{property.Name}Color(this IVisualElement<{type.Type.FullName}> element, System.Windows.Media.Color value)"); - } - else - { - builder.AppendLine($"public static async System.Threading.Tasks.Task Set{property.Name}Color(this IVisualElement element, System.Windows.Media.Color value) where T : {type.Type.FullName}"); - } - builder - .AppendLine(" {") - .Append(" ") - .AppendLine($"System.Windows.Media.SolidColorBrush? brush = await element.SetProperty(nameof({type.Type.FullName}.{property.Name}), new System.Windows.Media.SolidColorBrush(value));") - .Append(" ") - .AppendLine("return brush?.Color ?? default;") - .AppendLine(" }"); - } - - } - } - builder.AppendLine(" }"); - builder.AppendLine("}"); - - string fileName = $"XamlTest{type.Type.Name}GeneratedExtensions.g.cs"; - //System.IO.File.WriteAllText(@"D:\Dev\XAMLTest\XAMLTest\obj\" + fileName, builder.ToString()); - context.AddSource(fileName, builder.ToString()); - } - } - - public void Initialize(GeneratorInitializationContext context) - { -#if DEBUG - if (!System.Diagnostics.Debugger.IsAttached) - { - //Debugger.Launch(); - } -#endif - context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); + public void Initialize(IncrementalGeneratorInitializationContext context) + { +#if DEBUG + if (!System.Diagnostics.Debugger.IsAttached) + { + //Debugger.Launch(); + } +#endif + + // Create a provider for GenerateHelpersAttribute syntax nodes + var attributeProvider = context.SyntaxProvider + .CreateSyntaxProvider( + predicate: static (s, _) => IsSyntaxTargetForGeneration(s), + transform: static (ctx, _) => GetSemanticTargetForGeneration(ctx)) + .Where(static m => m is not null); + + // Combine with compilation to access type information + var compilationAndTypes = context.CompilationProvider.Combine(attributeProvider.Collect()); + + context.RegisterSourceOutput(compilationAndTypes, static (spc, source) => Execute(source.Left, source.Right, spc)); + } + + private static bool IsSyntaxTargetForGeneration(SyntaxNode node) + { + return node is AttributeSyntax attrib + && attrib.ArgumentList?.Arguments.Count >= 1; + } + + private static VisualElement? GetSemanticTargetForGeneration(GeneratorSyntaxContext context) + { + var attrib = (AttributeSyntax)context.Node; + + var typeInfo = context.SemanticModel.GetTypeInfo(attrib); + if (typeInfo.Type?.Name != "GenerateHelpersAttribute") + return null; + + if (attrib.ArgumentList?.Arguments.Count < 1) + return null; + + var typeArgument = (TypeOfExpressionSyntax)attrib.ArgumentList!.Arguments[0].Expression; + var info = context.SemanticModel.GetTypeInfo(typeArgument.Type); + if (info.Type is null) + return null; + + string? targetNamespace = null; + foreach (AttributeArgumentSyntax argumentExpression in attrib.ArgumentList.Arguments.Skip(1)) + { + string? target = argumentExpression.NameEquals?.Name.Identifier.Value?.ToString(); + + switch (target) + { + case "Namespace": + switch (argumentExpression.Expression) + { + case LiteralExpressionSyntax les: + targetNamespace = les.Token.Value?.ToString(); + break; + case MemberAccessExpressionSyntax maes: + targetNamespace = context.SemanticModel.GetConstantValue(maes.Name).Value?.ToString(); + break; + } + break; + } + } + + return ProcessTypeHierarchy(info.Type, targetNamespace ?? "XamlTest"); + } + + private static VisualElement? ProcessTypeHierarchy(ITypeSymbol rootType, string targetNamespace) + { + for (ITypeSymbol? type = rootType; + type is not null; + type = type.BaseType) + { + List properties = []; + + foreach (ISymbol member in type.GetMembers()) + { + if (member is IPropertySymbol property && + property.CanBeReferencedByName && + !property.IsStatic && + !property.IsOverride && + property.DeclaredAccessibility == Accessibility.Public && + !property.GetAttributes() + .Any(x => x.AttributeClass?.Name == "ObsoleteAttribute" || x.AttributeClass?.Name == "ExperimentalAttribute") && + !IgnoredTypes.Contains($"{property.Type}") && + !IsDelegate(property.Type)) + { + if (ShouldUseVisualElement(property.Type)) + { + properties.Add( + new Property( + property.Name, + $"XamlTest.IVisualElement<{property.Type}>?", + property.GetMethod is not null, + property.SetMethod is not null)); + } + else + { + string propertyType = $"{property.Type}"; + if (TypeRemap.TryGetValue(propertyType, out string? remappedType)) + { + propertyType = remappedType; + } + + if (property.Type.IsReferenceType && + !propertyType.EndsWith("?")) + { + propertyType += "?"; + } + + properties.Add( + new Property( + property.Name, + propertyType, + property.GetMethod is not null, + property.SetMethod is not null)); + } + } + } + if (properties.Any()) + { + string safeTypeName = GetSafeTypeName(type); + var visualElementType = new VisualElementType(safeTypeName, $"{type}", type.IsSealed || type.IsValueType); + return new VisualElement(targetNamespace, visualElementType, properties); + } + } + return null; + } + + private static void Execute(Compilation compilation, ImmutableArray types, SourceProductionContext context) + { + if (types.IsDefaultOrEmpty) + return; + + var validTypes = types.Where(x => x is not null).Cast().ToList(); + + HashSet ignoredTypes = new(); + foreach (var duplicatedAttributes in validTypes.GroupBy(x => x.Type.FullName).Where(g => g.Count() > 1)) + { + context.ReportDiagnostic(Diagnostic.Create(DuplicateAttributesWarning, Location.None, duplicatedAttributes.Key)); + ignoredTypes.Add(duplicatedAttributes.Key); + return; + } + + foreach (var type in validTypes) + { + if (ignoredTypes.Contains(type.Type.FullName)) continue; + + if (compilation.GetTypeByMetadataName($"{type.Namespace}.{type.Type.Name}GeneratedExtensions") is not null) + { + continue; + } + + StringBuilder builder = new(); + builder.AppendLine("#nullable enable"); + builder.AppendLine($"namespace {type.Namespace}"); + builder.AppendLine("{"); + builder.AppendLine($" public static partial class {type.Type.Name}GeneratedExtensions"); + builder.AppendLine(" {"); + foreach (var property in type.DependencyProperties) + { + builder.AppendLine(); + if (property.CanRead) + { + builder + .Append(" "); + + if (type.Type.IsFinal) + { + builder.AppendLine($"public static async System.Threading.Tasks.Task<{property.TypeFullName}> Get{property.Name}(this IVisualElement<{type.Type.FullName}> element)"); + } + else + { + builder.AppendLine($"public static async System.Threading.Tasks.Task<{property.TypeFullName}> Get{property.Name}(this IVisualElement element) where T : {type.Type.FullName}"); + } + builder.Append(" ") + .AppendLine($"=> await element.GetProperty<{property.TypeFullName}>(nameof({type.Type.FullName}.{property.Name}));"); + + if (property.TypeFullName.StartsWith("System.Windows.Media.SolidColorBrush") || + property.TypeFullName.StartsWith("System.Windows.Media.Brush")) + { + builder + .Append(" "); + if (type.Type.IsFinal) + { + builder.AppendLine($"public static async System.Threading.Tasks.Task Get{property.Name}Color(this IVisualElement<{type.Type.FullName}> element)"); + } + else + { + builder.AppendLine($"public static async System.Threading.Tasks.Task Get{property.Name}Color(this IVisualElement element) where T : {type.Type.FullName}"); + } + builder + .Append(" ") + .AppendLine($"=> await element.GetProperty(nameof({type.Type.FullName}.{property.Name}));"); + } + } + if (property.CanWrite) + { + builder + .Append(" "); + + if (type.Type.IsFinal) + { + builder.AppendLine($"public static async System.Threading.Tasks.Task<{property.TypeFullName}> Set{property.Name}(this IVisualElement<{type.Type.FullName}> element, {property.TypeFullName} value)"); + } + else + { + builder.AppendLine($"public static async System.Threading.Tasks.Task<{property.TypeFullName}> Set{property.Name}(this IVisualElement element, {property.TypeFullName} value) where T : {type.Type.FullName}"); + } + builder + .Append(" ") + .AppendLine($"=> await element.SetProperty(nameof({type.Type.FullName}.{property.Name}), value);"); + + if (property.TypeFullName.StartsWith("System.Windows.Media.SolidColorBrush") || + property.TypeFullName.StartsWith("System.Windows.Media.Brush")) + { + builder + .Append(" "); + if (type.Type.IsFinal) + { + builder.AppendLine($"public static async System.Threading.Tasks.Task Set{property.Name}Color(this IVisualElement<{type.Type.FullName}> element, System.Windows.Media.Color value)"); + } + else + { + builder.AppendLine($"public static async System.Threading.Tasks.Task Set{property.Name}Color(this IVisualElement element, System.Windows.Media.Color value) where T : {type.Type.FullName}"); + } + builder + .AppendLine(" {") + .Append(" ") + .AppendLine($"System.Windows.Media.SolidColorBrush? brush = await element.SetProperty(nameof({type.Type.FullName}.{property.Name}), new System.Windows.Media.SolidColorBrush(value));") + .Append(" ") + .AppendLine("return brush?.Color ?? default;") + .AppendLine(" }"); + } + + } + } + builder.AppendLine(" }"); + builder.AppendLine("}"); + + string fileName = $"XamlTest{type.Type.Name}GeneratedExtensions.g.cs"; + //System.IO.File.WriteAllText(@"D:\Dev\XAMLTest\XAMLTest\obj\" + fileName, builder.ToString()); + context.AddSource(fileName, builder.ToString()); + } + } + + private static Dictionary TypeRemap { get; } = new() + { + { "System.Windows.Controls.ColumnDefinitionCollection", "System.Collections.Generic.IList" }, + { "System.Windows.Controls.RowDefinitionCollection", "System.Collections.Generic.IList" } + }; + + private static HashSet IgnoredTypes { get; } = new() + { + "System.Windows.TriggerCollection", + "System.Windows.Media.CacheMode", + "System.Windows.Input.CommandBindingCollection", + "System.Windows.Media.Effects.Effect", + "System.Windows.Input.InputBindingCollection", + "System.Collections.Generic.IEnumerable", + "System.Windows.DependencyObjectType", + "System.Windows.Threading.Dispatcher", + "System.Windows.TextDecorationCollection", + "System.Windows.Media.TextEffectCollection", + "System.Windows.Data.BindingGroup", + "System.Windows.Style", + "System.Windows.ResourceDictionary", + "System.Windows.DataTemplate", + "System.Windows.Controls.DataTemplateSelector", + "System.Windows.Controls.ControlTemplate", + "System.Windows.Controls.CalendarBlackoutDatesCollection", + "System.Windows.Controls.SelectedDatesCollection", + "System.Windows.Controls.UIElementCollection", + "System.Collections.ObjectModel.ObservableCollection", + "System.Windows.Controls.GroupStyleSelector", + "System.Windows.Controls.ItemContainerGenerator", + "System.Windows.Controls.StyleSelector", + "System.Windows.Controls.ItemCollection", + "System.Windows.Controls.ItemsPanelTemplate", + "System.Collections.ObjectModel.ObservableCollection", + "System.Collections.ObjectModel.ObservableCollection", + "System.Collections.Generic.IList", + "System.Collections.IList", + "System.Windows.Controls.Primitives.IItemContainerGenerator", + "System.Windows.Documents.IDocumentPaginatorSource", + "System.Windows.Documents.Typography", + "System.Windows.Documents.InlineCollection", + "System.Windows.Documents.TextPointer", + "System.Windows.Controls.ItemContainerTemplateSelector", + "System.Windows.Controls.DataGridCellInfo", + "System.Windows.Documents.DocumentPage", + "System.Windows.Documents.DocumentPaginator", + "System.Windows.Documents.TextSelection", + "System.Windows.Navigation.NavigationService", + "System.Windows.Ink.DrawingAttributes", + "System.Windows.Input.StylusPointDescription", + "System.Windows.Ink.StylusShape", + "System.Collections.Generic.IEnumerable", + "System.Windows.Media.MediaClock", + "System.Windows.IInputElement", + "System.Collections.ObjectModel.Collection", + "System.Windows.WindowCollection" + }; + + private static string GetSafeTypeName(ITypeSymbol typeSymbol) + { + string safeTypeName = typeSymbol.Name; + + if (typeSymbol is INamedTypeSymbol { TypeArguments.Length: > 0 } genericSymbol) + { + safeTypeName += $"_{string.Join("_", genericSymbol.TypeArguments.Select(x => GetSafeTypeName(x)))}"; + } + return safeTypeName; + } + + private static bool ShouldUseVisualElement(ITypeSymbol typeSymbol) + { + for (ITypeSymbol? type = typeSymbol; + type != null; + type = type.BaseType) + { + switch($"{type}") + { + case "System.Windows.Media.Brush": return false; + case "System.Windows.DependencyObject": return true; + } + } + return false; + } + + private static bool IsDelegate(ITypeSymbol typeSymbol) + => Is(typeSymbol, "System.Delegate"); + + private static bool Is(ITypeSymbol typeSymbol, string targetType) + { + for (ITypeSymbol? type = typeSymbol; + type != null; + type = type.BaseType) + { + if ($"{type}" == targetType) + { + return true; + } + } + return false; } } -public record VisualElement( - string Namespace, - VisualElementType Type, - IReadOnlyList DependencyProperties) +public record VisualElement( + string Namespace, + VisualElementType Type, + IReadOnlyList DependencyProperties) +{ } + +public record VisualElementType(string Name, string FullName, bool IsFinal) +{ } + +public record Property(string Name, string TypeFullName, bool CanRead, bool CanWrite) { } - -public record VisualElementType(string Name, string FullName, bool IsFinal) -{ } - -public record Property(string Name, string TypeFullName, bool CanRead, bool CanWrite) -{ } - -public class SyntaxReceiver : ISyntaxContextReceiver -{ - private static Dictionary TypeRemap { get; } = new() - { - { "System.Windows.Controls.ColumnDefinitionCollection", "System.Collections.Generic.IList" }, - { "System.Windows.Controls.RowDefinitionCollection", "System.Collections.Generic.IList" } - }; - - private static HashSet IgnoredTypes { get; } = new() - { - "System.Windows.TriggerCollection", - "System.Windows.Media.CacheMode", - "System.Windows.Input.CommandBindingCollection", - "System.Windows.Media.Effects.Effect", - "System.Windows.Input.InputBindingCollection", - "System.Collections.Generic.IEnumerable", - "System.Windows.DependencyObjectType", - "System.Windows.Threading.Dispatcher", - "System.Windows.TextDecorationCollection", - "System.Windows.Media.TextEffectCollection", - "System.Windows.Data.BindingGroup", - "System.Windows.Style", - "System.Windows.ResourceDictionary", - "System.Windows.DataTemplate", - "System.Windows.Controls.DataTemplateSelector", - "System.Windows.Controls.ControlTemplate", - "System.Windows.Controls.CalendarBlackoutDatesCollection", - "System.Windows.Controls.SelectedDatesCollection", - "System.Windows.Controls.UIElementCollection", - "System.Collections.ObjectModel.ObservableCollection", - "System.Windows.Controls.GroupStyleSelector", - "System.Windows.Controls.ItemContainerGenerator", - "System.Windows.Controls.StyleSelector", - "System.Windows.Controls.ItemCollection", - "System.Windows.Controls.ItemsPanelTemplate", - "System.Collections.ObjectModel.ObservableCollection", - "System.Collections.ObjectModel.ObservableCollection", - "System.Collections.Generic.IList", - "System.Collections.IList", - "System.Windows.Controls.Primitives.IItemContainerGenerator", - "System.Windows.Documents.IDocumentPaginatorSource", - "System.Windows.Documents.Typography", - "System.Windows.Documents.InlineCollection", - "System.Windows.Documents.TextPointer", - "System.Windows.Controls.ItemContainerTemplateSelector", - "System.Windows.Controls.DataGridCellInfo", - "System.Windows.Documents.DocumentPage", - "System.Windows.Documents.DocumentPaginator", - "System.Windows.Documents.TextSelection", - "System.Windows.Navigation.NavigationService", - "System.Windows.Ink.DrawingAttributes", - "System.Windows.Input.StylusPointDescription", - "System.Windows.Ink.StylusShape", - "System.Collections.Generic.IEnumerable", - "System.Windows.Media.MediaClock", - "System.Windows.IInputElement", - "System.Collections.ObjectModel.Collection", - "System.Windows.WindowCollection" - }; - private List Elements { get; } = new(); - public IReadOnlyList GeneratedTypes => Elements; - - public void OnVisitSyntaxNode(GeneratorSyntaxContext context) - { - if (context.Node is AttributeSyntax attrib - && attrib.ArgumentList?.Arguments.Count >= 1 - && context.SemanticModel.GetTypeInfo(attrib).Type?.Name == "GenerateHelpersAttribute") - { - TypeOfExpressionSyntax typeArgument = (TypeOfExpressionSyntax)attrib.ArgumentList.Arguments[0].Expression; - TypeInfo info = context.SemanticModel.GetTypeInfo(typeArgument.Type); - if (info.Type is null) return; - - string? targetNamespace = null; - foreach (AttributeArgumentSyntax argumentExpression in attrib.ArgumentList.Arguments.Skip(1)) - { - string? target = argumentExpression.NameEquals?.Name.Identifier.Value?.ToString(); - - switch (target) - { - case "Namespace": - switch (argumentExpression.Expression) - { - case LiteralExpressionSyntax les: - targetNamespace = les.Token.Value?.ToString(); - break; - case MemberAccessExpressionSyntax maes: - targetNamespace = context.SemanticModel.GetConstantValue(maes.Name).Value?.ToString(); - break; - } - break; - } - } - - for (ITypeSymbol? type = info.Type; - type is not null; - type = type.BaseType) - { - List properties = []; - - if (Elements.Any(x => x.Type.FullName == $"{type}")) continue; - - foreach (ISymbol member in type.GetMembers()) - { - if (member is IPropertySymbol property && - property.CanBeReferencedByName && - !property.IsStatic && - !property.IsOverride && - property.DeclaredAccessibility == Accessibility.Public && - !property.GetAttributes() - .Any(x => x.AttributeClass?.Name == "ObsoleteAttribute" || x.AttributeClass?.Name == "ExperimentalAttribute") && - !IgnoredTypes.Contains($"{property.Type}") && - !IsDelegate(property.Type)) - { - if (ShouldUseVisualElement(property.Type)) - { - properties.Add( - new Property( - property.Name, - $"XamlTest.IVisualElement<{property.Type}>?", - property.GetMethod is not null, - property.SetMethod is not null)); - } - else - { - string propertyType = $"{property.Type}"; - if (TypeRemap.TryGetValue(propertyType, out string? remappedType)) - { - propertyType = remappedType; - } - - if (property.Type.IsReferenceType && - !propertyType.EndsWith("?")) - { - propertyType += "?"; - } - - properties.Add( - new Property( - property.Name, - propertyType, - property.GetMethod is not null, - property.SetMethod is not null)); - } - } - } - if (properties.Any()) - { - string safeTypeName = GetSafeTypeName(type); - var visualElementType = new VisualElementType(safeTypeName, $"{type}", type.IsSealed || type.IsValueType); - Elements.Add(new VisualElement(targetNamespace ?? "XamlTest", visualElementType, properties)); - } - } - } - - static string GetSafeTypeName(ITypeSymbol typeSymbol) - { - string safeTypeName = typeSymbol.Name; - - if (typeSymbol is INamedTypeSymbol { TypeArguments.Length: > 0 } genericSymbol) - { - safeTypeName += $"_{string.Join("_", genericSymbol.TypeArguments.Select(x => GetSafeTypeName(x)))}"; - } - return safeTypeName; - } - - static bool ShouldUseVisualElement(ITypeSymbol typeSymbol) - { - for (ITypeSymbol? type = typeSymbol; - type != null; - type = type.BaseType) - { - switch($"{type}") - { - case "System.Windows.Media.Brush": return false; - case "System.Windows.DependencyObject": return true; - } - } - return false; - } - } - - private static bool IsDelegate(ITypeSymbol typeSymbol) - => Is(typeSymbol, "System.Delegate"); - - private static bool Is(ITypeSymbol typeSymbol, string targetType) - { - for (ITypeSymbol? type = typeSymbol; - type != null; - type = type.BaseType) - { - if ($"{type}" == targetType) - { - return true; - } - } - return false; - } - -} diff --git a/XAMLTest.UnitTestGenerator/UnitTestGenerator.cs b/XAMLTest.UnitTestGenerator/UnitTestGenerator.cs index 9c36371..c6bb58c 100644 --- a/XAMLTest.UnitTestGenerator/UnitTestGenerator.cs +++ b/XAMLTest.UnitTestGenerator/UnitTestGenerator.cs @@ -1,22 +1,71 @@ -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using TypeInfo = Microsoft.CodeAnalysis.TypeInfo; - -namespace XAMLTest.UnitTestGenerator; - -[Generator] -public class UnitTestGenerator : ISourceGenerator +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using System.Text; +using TypeInfo = Microsoft.CodeAnalysis.TypeInfo; + +namespace XAMLTest.UnitTestGenerator; + +[Generator] +public class UnitTestGenerator : IIncrementalGenerator { - public void Execute(GeneratorExecutionContext context) - { -#if DEBUG - if (!Debugger.IsAttached) - { - //Debugger.Launch(); - } -#endif - SyntaxReceiver rx = (SyntaxReceiver)context.SyntaxContextReceiver!; - foreach (TypeInfo targetType in rx.GeneratedTypes) + public void Initialize(IncrementalGeneratorInitializationContext context) + { + // Create a provider for GenerateTestsAttribute syntax nodes + var attributeProvider = context.SyntaxProvider + .CreateSyntaxProvider( + predicate: static (s, _) => IsSyntaxTargetForGeneration(s), + transform: static (ctx, _) => GetSemanticTargetForGeneration(ctx)) + .Where(static m => m is not null); + + // Combine with compilation to access type information + var compilationAndTypes = context.CompilationProvider.Combine(attributeProvider.Collect()); + + context.RegisterSourceOutput(compilationAndTypes, static (spc, source) => Execute(source.Left, source.Right, spc)); + } + + private static bool IsSyntaxTargetForGeneration(SyntaxNode node) + { + return node is AttributeSyntax attrib + && attrib.ArgumentList?.Arguments.Count >= 1; + } + + private static TypeInfo? GetSemanticTargetForGeneration(GeneratorSyntaxContext context) + { + var attrib = (AttributeSyntax)context.Node; + + var typeInfo = context.SemanticModel.GetTypeInfo(attrib); + if (typeInfo.Type?.Name != "GenerateTestsAttribute") + return null; + + if (attrib.ArgumentList?.Arguments.Count < 1) + return null; + + var typeArgument = (TypeOfExpressionSyntax)attrib.ArgumentList!.Arguments[0].Expression; + var info = context.SemanticModel.GetTypeInfo(typeArgument.Type); + if (info.Type is null) + return null; + + return info; + } + + private static void Execute(Compilation compilation, ImmutableArray types, SourceProductionContext context) + { +#if DEBUG + if (!Debugger.IsAttached) + { + //Debugger.Launch(); + } +#endif + + if (types.IsDefaultOrEmpty) + return; + + var validTypes = types.Where(x => x is not null).Cast().ToList(); + + foreach (TypeInfo targetType in validTypes) { if (targetType.Type?.IsAbstract == true) continue; @@ -24,7 +73,7 @@ public void Execute(GeneratorExecutionContext context) const string suffix = "GeneratedExtensions"; string targetTypeFullName = $"{targetType.Type}"; string targetTypeName = targetType.Type!.Name; - var extensionClass = context.Compilation.GetTypeByMetadataName($"XamlTest.{targetTypeName}{suffix}"); + var extensionClass = compilation.GetTypeByMetadataName($"XamlTest.{targetTypeName}{suffix}"); if (extensionClass is null) continue; string variableTargetTypeName = @@ -117,10 +166,11 @@ public static async Task TestCleanup() //System.IO.File.WriteAllText($@"D:\Dev\XAMLTest\XAMLTest.UnitTestGenerator\obj\{className}.cs", sb.ToString()); - context.AddSource($"{className}.cs", sb.ToString()); - } - - static IEnumerable<(IMethodSymbol, IFieldSymbol)> GetTestMethods(INamedTypeSymbol extensionClass) + context.AddSource($"{className}.cs", sb.ToString()); + } + } + + static IEnumerable<(IMethodSymbol, IFieldSymbol)> GetTestMethods(INamedTypeSymbol extensionClass) { for (INamedTypeSymbol? type = extensionClass; type != null; @@ -140,9 +190,9 @@ public static async Task TestCleanup() yield return (getMethod, dependencyProperty); } } - } - - static string GetAssertion(string propertyName, string returnType, IFieldSymbol dependencyProperty) + } + + static string GetAssertion(string propertyName, string returnType, IFieldSymbol dependencyProperty) { return propertyName switch { @@ -153,31 +203,6 @@ static string GetAssertion(string propertyName, string returnType, IFieldSymbol Assert.AreEqual(expected, actual); """, _ => $"Assert.AreEqual(default({returnType}), actual);", - }; - } - } - - public void Initialize(GeneratorInitializationContext context) - { - context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); - } - - public class SyntaxReceiver : ISyntaxContextReceiver - { - public List GeneratedTypes { get; } = new(); - - public void OnVisitSyntaxNode(GeneratorSyntaxContext context) - { - if (context.Node is AttributeSyntax attrib - && attrib.ArgumentList?.Arguments.Count >= 1 - && context.SemanticModel.GetTypeInfo(attrib).Type?.Name == "GenerateTestsAttribute") - { - TypeOfExpressionSyntax typeArgument = (TypeOfExpressionSyntax)attrib.ArgumentList.Arguments[0].Expression; - TypeInfo info = context.SemanticModel.GetTypeInfo(typeArgument.Type); - if (info.Type is null) return; - - GeneratedTypes.Add(info); - } - } + }; } }