diff --git a/Avalonia.sln b/Avalonia.sln index cd7e56e9d44..9aec50be47f 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -302,8 +302,15 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.RenderTests.WpfCompare", "tests\Avalonia.RenderTests.WpfCompare\Avalonia.RenderTests.WpfCompare.csproj", "{9AE1B827-21AC-4063-AB22-C8804B7F931E}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Win32.Automation", "src\Windows\Avalonia.Win32.Automation\Avalonia.Win32.Automation.csproj", "{0097673D-DBCE-476E-82FE-E78A56E58AA2}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XEmbedSample", "samples\XEmbedSample\XEmbedSample.csproj", "{255614F5-CB64-4ECA-A026-E0B1AF6A2EF4}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.CssSrcGen", "src\Avalonia.CssSrcGen\Avalonia.CssSrcGen\Avalonia.CssSrcGen.csproj", "{F7797895-7400-4F70-BDFC-05FDF4B3020D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.CssSrcGen.Sample", "src\Avalonia.CssSrcGen\Avalonia.CssSrcGen.Sample\Avalonia.CssSrcGen.Sample.csproj", "{3D8D69F6-79FC-45C2-B7EA-6213E4AC260E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.CssSrcGen.Tests", "src\Avalonia.CssSrcGen\Avalonia.CssSrcGen.Tests\Avalonia.CssSrcGen.Tests.csproj", "{B9C3D58D-94D9-4B9D-8E61-E5A452F747CB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -712,6 +719,18 @@ Global {255614F5-CB64-4ECA-A026-E0B1AF6A2EF4}.Debug|Any CPU.Build.0 = Debug|Any CPU {255614F5-CB64-4ECA-A026-E0B1AF6A2EF4}.Release|Any CPU.ActiveCfg = Release|Any CPU {255614F5-CB64-4ECA-A026-E0B1AF6A2EF4}.Release|Any CPU.Build.0 = Release|Any CPU + {F7797895-7400-4F70-BDFC-05FDF4B3020D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F7797895-7400-4F70-BDFC-05FDF4B3020D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F7797895-7400-4F70-BDFC-05FDF4B3020D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F7797895-7400-4F70-BDFC-05FDF4B3020D}.Release|Any CPU.Build.0 = Release|Any CPU + {3D8D69F6-79FC-45C2-B7EA-6213E4AC260E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3D8D69F6-79FC-45C2-B7EA-6213E4AC260E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3D8D69F6-79FC-45C2-B7EA-6213E4AC260E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3D8D69F6-79FC-45C2-B7EA-6213E4AC260E}.Release|Any CPU.Build.0 = Release|Any CPU + {B9C3D58D-94D9-4B9D-8E61-E5A452F747CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B9C3D58D-94D9-4B9D-8E61-E5A452F747CB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B9C3D58D-94D9-4B9D-8E61-E5A452F747CB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B9C3D58D-94D9-4B9D-8E61-E5A452F747CB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -801,6 +820,9 @@ Global {9AE1B827-21AC-4063-AB22-C8804B7F931E} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {0097673D-DBCE-476E-82FE-E78A56E58AA2} = {B39A8919-9F95-48FE-AD7B-76E08B509888} {255614F5-CB64-4ECA-A026-E0B1AF6A2EF4} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {F7797895-7400-4F70-BDFC-05FDF4B3020D} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {3D8D69F6-79FC-45C2-B7EA-6213E4AC260E} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {B9C3D58D-94D9-4B9D-8E61-E5A452F747CB} = {9B9E3891-2366-4253-A952-D08BCEB71098} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index edcbcbe988d..ccb6dd26b4d 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -35,6 +35,10 @@ + + + + diff --git a/samples/ControlCatalog/Pages/ButtonsPage.xaml b/samples/ControlCatalog/Pages/ButtonsPage.xaml index d0503c44ae6..5497b063bac 100644 --- a/samples/ControlCatalog/Pages/ButtonsPage.xaml +++ b/samples/ControlCatalog/Pages/ButtonsPage.xaml @@ -1,6 +1,7 @@ + x:Class="ControlCatalog.Pages.ButtonsPage" + Styles="{}"> @@ -17,8 +18,8 @@ - - + + diff --git a/samples/ControlCatalog/Pages/ButtonsPage.xaml.cs b/samples/ControlCatalog/Pages/ButtonsPage.xaml.cs index 2d63f1fee99..64da58a2547 100644 --- a/samples/ControlCatalog/Pages/ButtonsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ButtonsPage.xaml.cs @@ -1,16 +1,17 @@ using Avalonia.Controls; using Avalonia.Markup.Xaml; +using Generators; namespace ControlCatalog.Pages { - public class ButtonsPage : UserControl + [CssGenerator] + public partial class ButtonsPage : UserControl { private int repeatButtonClickCount = 0; public ButtonsPage() { InitializeComponent(); - this.Get("RepeatButton").Click += OnRepeatButtonClick; } diff --git a/src/Avalonia.CssSrcGen/Avalonia.CssSrcGen.Sample/Avalonia.CssSrcGen.Sample.csproj b/src/Avalonia.CssSrcGen/Avalonia.CssSrcGen.Sample/Avalonia.CssSrcGen.Sample.csproj new file mode 100644 index 00000000000..1697c2ed1f9 --- /dev/null +++ b/src/Avalonia.CssSrcGen/Avalonia.CssSrcGen.Sample/Avalonia.CssSrcGen.Sample.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + Avalonia.CssSrcGen.Sample + + + + + + + + + + + + diff --git a/src/Avalonia.CssSrcGen/Avalonia.CssSrcGen.Sample/DDD.UbiquitousLanguageRegistry.txt b/src/Avalonia.CssSrcGen/Avalonia.CssSrcGen.Sample/DDD.UbiquitousLanguageRegistry.txt new file mode 100644 index 00000000000..aea323f2486 --- /dev/null +++ b/src/Avalonia.CssSrcGen/Avalonia.CssSrcGen.Sample/DDD.UbiquitousLanguageRegistry.txt @@ -0,0 +1,5 @@ +Customer +Product +Stock +Shop +Employee \ No newline at end of file diff --git a/src/Avalonia.CssSrcGen/Avalonia.CssSrcGen.Sample/Examples.cs b/src/Avalonia.CssSrcGen/Avalonia.CssSrcGen.Sample/Examples.cs new file mode 100644 index 00000000000..c7e7e49e346 --- /dev/null +++ b/src/Avalonia.CssSrcGen/Avalonia.CssSrcGen.Sample/Examples.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using Entities; + +namespace Avalonia.CssSrcGen.Sample; + +// This code will not compile until you build the project with the Source Generators + +public class Examples +{ + // Create generated entities, based on DDD.UbiquitousLanguageRegistry.txt + public object[] CreateEntities() + { + return new object[] { new Customer(), new Employee(), new Product(), new Shop(), new Stock() }; + } + + // Execute generated method Report + public IEnumerable CreateEntityReport(SampleEntity entity) + { + return entity.Report(); + } +} diff --git a/src/Avalonia.CssSrcGen/Avalonia.CssSrcGen.Sample/SampleEntity.cs b/src/Avalonia.CssSrcGen/Avalonia.CssSrcGen.Sample/SampleEntity.cs new file mode 100644 index 00000000000..742e5af8062 --- /dev/null +++ b/src/Avalonia.CssSrcGen/Avalonia.CssSrcGen.Sample/SampleEntity.cs @@ -0,0 +1,12 @@ +using Generators; + +namespace Avalonia.CssSrcGen.Sample; + +// This code will not compile until you build the project with the Source Generators + +[Report] +public partial class SampleEntity +{ + public int Id { get; } = 42; + public string? Name { get; } = "Sample"; +} diff --git a/src/Avalonia.CssSrcGen/Avalonia.CssSrcGen.Tests/Avalonia.CssSrcGen.Tests.csproj b/src/Avalonia.CssSrcGen/Avalonia.CssSrcGen.Tests/Avalonia.CssSrcGen.Tests.csproj new file mode 100644 index 00000000000..26b1c707021 --- /dev/null +++ b/src/Avalonia.CssSrcGen/Avalonia.CssSrcGen.Tests/Avalonia.CssSrcGen.Tests.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + + false + + Avalonia.CssSrcGen.Tests + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/src/Avalonia.CssSrcGen/Avalonia.CssSrcGen.Tests/SourceGeneratorWithAdditionalFilesTests.cs b/src/Avalonia.CssSrcGen/Avalonia.CssSrcGen.Tests/SourceGeneratorWithAdditionalFilesTests.cs new file mode 100644 index 00000000000..eedf580f602 --- /dev/null +++ b/src/Avalonia.CssSrcGen/Avalonia.CssSrcGen.Tests/SourceGeneratorWithAdditionalFilesTests.cs @@ -0,0 +1,46 @@ +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using Microsoft.CodeAnalysis; +using Avalonia.CssSrcGen.Tests.Utils; +using Microsoft.CodeAnalysis.CSharp; +using Xunit; + +namespace Avalonia.CssSrcGen.Tests; + +public class SourceGeneratorWithAdditionalFilesTests +{ + private const string DddRegistryText = @"User +Document +Customer"; + + [Fact] + public void GenerateClassesBasedOnDDDRegistry() + { + // Create an instance of the source generator. + var generator = new SourceGeneratorWithAdditionalFiles(); + + // Source generators should be tested using 'GeneratorDriver'. + GeneratorDriver driver = CSharpGeneratorDriver.Create(generator); + + // Add the additional file separately from the compilation. + driver = driver.AddAdditionalTexts( + ImmutableArray.Create( + new TestAdditionalFile("./DDD.UbiquitousLanguageRegistry.txt", DddRegistryText)) + ); + + // To run generators, we can use an empty compilation. + var compilation = CSharpCompilation.Create(nameof(SourceGeneratorWithAdditionalFilesTests)); + + // Run generators. Don't forget to use the new compilation rather than the previous one. + driver.RunGeneratorsAndUpdateCompilation(compilation, out var newCompilation, out _); + + // Retrieve all files in the compilation. + var generatedFiles = newCompilation.SyntaxTrees + .Select(t => Path.GetFileName(t.FilePath)) + .ToArray(); + + // In this case, it is enough to check the file name. + Assert.Equivalent(new[] { "User.g.cs", "Document.g.cs", "Customer.g.cs" }, generatedFiles); + } +} diff --git a/src/Avalonia.CssSrcGen/Avalonia.CssSrcGen.Tests/SourceGeneratorWithAttributesTests.cs b/src/Avalonia.CssSrcGen/Avalonia.CssSrcGen.Tests/SourceGeneratorWithAttributesTests.cs new file mode 100644 index 00000000000..77d6343a6d3 --- /dev/null +++ b/src/Avalonia.CssSrcGen/Avalonia.CssSrcGen.Tests/SourceGeneratorWithAttributesTests.cs @@ -0,0 +1,67 @@ +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Xunit; + +namespace Avalonia.CssSrcGen.Tests; + +public class SourceGeneratorWithAttributesTests +{ + private const string VectorClassText = @" +namespace TestNamespace; + +[Generators.Report] +public partial class Vector3 +{ + public float X { get; set; } + public float Y { get; set; } + public float Z { get; set; } +}"; + + private const string ExpectedGeneratedClassText = @"// + +using System; +using System.Collections.Generic; + +namespace TestNamespace; + +partial class Vector3 +{ + public IEnumerable Report() + { + yield return $""X:{this.X}""; + yield return $""Y:{this.Y}""; + yield return $""Z:{this.Z}""; + } +} +"; + + [Fact] + public void GenerateReportMethod() + { + // Create an instance of the source generator. + var generator = new SourceGeneratorWithAttributes(); + + // Source generators should be tested using 'GeneratorDriver'. + var driver = CSharpGeneratorDriver.Create(generator); + + // We need to create a compilation with the required source code. + var compilation = CSharpCompilation.Create(nameof(SourceGeneratorWithAdditionalFilesTests), + new[] { CSharpSyntaxTree.ParseText(VectorClassText) }, + new[] + { + // To support 'System.Attribute' inheritance, add reference to 'System.Private.CoreLib'. + MetadataReference.CreateFromFile(typeof(object).Assembly.Location) + }); + + // Run generators and retrieve all results. + var runResult = driver.RunGenerators(compilation).GetRunResult(); + + // All generated files can be found in 'RunResults.GeneratedTrees'. + var generatedFileSyntax = runResult.GeneratedTrees.Single(t => t.FilePath.EndsWith("Vector3.g.cs")); + + // Complex generators should be tested using text comparison. + Assert.Equal(ExpectedGeneratedClassText, generatedFileSyntax.GetText().ToString(), + ignoreLineEndingDifferences: true); + } +} diff --git a/src/Avalonia.CssSrcGen/Avalonia.CssSrcGen.Tests/Utils/TestAdditionalFile.cs b/src/Avalonia.CssSrcGen/Avalonia.CssSrcGen.Tests/Utils/TestAdditionalFile.cs new file mode 100644 index 00000000000..291da2e0b54 --- /dev/null +++ b/src/Avalonia.CssSrcGen/Avalonia.CssSrcGen.Tests/Utils/TestAdditionalFile.cs @@ -0,0 +1,20 @@ +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace Avalonia.CssSrcGen.Tests.Utils; + +public class TestAdditionalFile : AdditionalText +{ + private readonly SourceText _text; + + public TestAdditionalFile(string path, string text) + { + Path = path; + _text = SourceText.From(text); + } + + public override SourceText GetText(CancellationToken cancellationToken = new()) => _text; + + public override string Path { get; } +} diff --git a/src/Avalonia.CssSrcGen/Avalonia.CssSrcGen/Avalonia.CssSrcGen.csproj b/src/Avalonia.CssSrcGen/Avalonia.CssSrcGen/Avalonia.CssSrcGen.csproj new file mode 100644 index 00000000000..b48a749386a --- /dev/null +++ b/src/Avalonia.CssSrcGen/Avalonia.CssSrcGen/Avalonia.CssSrcGen.csproj @@ -0,0 +1,26 @@ + + + + netstandard2.0 + false + enable + latest + + true + true + + Avalonia.CssSrcGen + Avalonia.CssSrcGen + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + diff --git a/src/Avalonia.CssSrcGen/Avalonia.CssSrcGen/Properties/launchSettings.json b/src/Avalonia.CssSrcGen/Avalonia.CssSrcGen/Properties/launchSettings.json new file mode 100644 index 00000000000..6347fa5898f --- /dev/null +++ b/src/Avalonia.CssSrcGen/Avalonia.CssSrcGen/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "DebugRoslynSourceGenerator": { + "commandName": "DebugRoslynComponent", + "targetProject": "../Avalonia.CssSrcGen.Sample/Avalonia.CssSrcGen.Sample.csproj" + } + } +} \ No newline at end of file diff --git a/src/Avalonia.CssSrcGen/Avalonia.CssSrcGen/Readme.md b/src/Avalonia.CssSrcGen/Avalonia.CssSrcGen/Readme.md new file mode 100644 index 00000000000..1d00f674163 --- /dev/null +++ b/src/Avalonia.CssSrcGen/Avalonia.CssSrcGen/Readme.md @@ -0,0 +1,29 @@ +# Roslyn Source Generators Sample + +A set of three projects that illustrates Roslyn source generators. Enjoy this template to learn from and modify source generators for your own needs. + +## Content +### Avalonia.CssSrcGen +A .NET Standard project with implementations of sample source generators. +**You must build this project to see the result (generated code) in the IDE.** + +- [SampleSourceGenerator.cs](SampleSourceGenerator.cs): A source generator that creates C# classes based on a text file (in this case, Domain Driven Design ubiquitous language registry). +- [SampleIncrementalSourceGenerator.cs](SampleIncrementalSourceGenerator.cs): A source generator that creates a custom report based on class properties. The target class should be annotated with the `Generators.ReportAttribute` attribute. + +### Avalonia.CssSrcGen.Sample +A project that references source generators. Note the parameters of `ProjectReference` in [Avalonia.CssSrcGen.Sample.csproj](../Avalonia.CssSrcGen.Sample/Avalonia.CssSrcGen.Sample.csproj), they make sure that the project is referenced as a set of source generators. + +### Avalonia.CssSrcGen.Tests +Unit tests for source generators. The easiest way to develop language-related features is to start with unit tests. + +## How To? +### How to debug? +- Use the [launchSettings.json](Properties/launchSettings.json) profile. +- Debug tests. + +### How can I determine which syntax nodes I should expect? +Consider installing the Roslyn syntax tree viewer plugin [Rossynt](https://plugins.jetbrains.com/plugin/16902-rossynt/). + +### How to learn more about wiring source generators? +Watch the walkthrough video: [Let’s Build an Incremental Source Generator With Roslyn, by Stefan Pölz](https://youtu.be/azJm_Y2nbAI) +The complete set of information is available in [Source Generators Cookbook](https://github.com/dotnet/roslyn/blob/main/docs/features/source-generators.cookbook.md). \ No newline at end of file diff --git a/src/Avalonia.CssSrcGen/Avalonia.CssSrcGen/SourceGeneratorWithAdditionalFiles.cs b/src/Avalonia.CssSrcGen/Avalonia.CssSrcGen/SourceGeneratorWithAdditionalFiles.cs new file mode 100644 index 00000000000..876008adbc4 --- /dev/null +++ b/src/Avalonia.CssSrcGen/Avalonia.CssSrcGen/SourceGeneratorWithAdditionalFiles.cs @@ -0,0 +1,51 @@ +using System.Collections.Immutable; +using System.IO; +using Microsoft.CodeAnalysis; + +namespace Avalonia.CssSrcGen; + +/// +/// A sample source generator that creates C# classes based on the text file (in this case, Domain Driven Design ubiquitous language registry). +/// When using a simple text file as a baseline, we can create a non-incremental source generator. +/// +[Generator] +public class SourceGeneratorWithAdditionalFiles : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var provider = context.AdditionalTextsProvider + .Where(f => Path.GetFileName(f.Path) == "DDD.UbiquitousLanguageRegistry.txt") + .Collect(); + + context.RegisterSourceOutput(provider, GenerateCode); + } + + private void GenerateCode(SourceProductionContext context, ImmutableArray files) + { + foreach (var file in files) + { + // Get the text of the file. + var lines = file.GetText(context.CancellationToken)?.ToString().Split('\n'); + if (lines == null) + continue; + + foreach (var line in lines) + { + var className = line.Trim(); + + // Build up the source code. + string source = $@"// + +namespace Entities +{{ + public partial class {className} + {{ + }} +}} +"; + // Add the source code to the compilation. + context.AddSource($"{className}.g.cs", source); + } + } + } +} diff --git a/src/Avalonia.CssSrcGen/Avalonia.CssSrcGen/SourceGeneratorWithAttributes.cs b/src/Avalonia.CssSrcGen/Avalonia.CssSrcGen/SourceGeneratorWithAttributes.cs new file mode 100644 index 00000000000..5572f309b63 --- /dev/null +++ b/src/Avalonia.CssSrcGen/Avalonia.CssSrcGen/SourceGeneratorWithAttributes.cs @@ -0,0 +1,159 @@ +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + + +namespace Avalonia.CssSrcGen; + +/// +/// A sample source generator that creates a custom report based on class properties. The target class should be annotated with the 'Generators.ReportAttribute' attribute. +/// When using the source code as a baseline, an incremental source generator is preferable because it reduces the performance overhead. +/// +[Generator] +public class SourceGeneratorWithAttributes : IIncrementalGenerator +{ + private const string Namespace = "Generators"; + private const string AttributeName = "CssGeneratorAttribute"; + + private const string AttributeSourceCode = $@"// + +namespace {Namespace} +{{ + [System.AttributeUsage(System.AttributeTargets.Class)] + public class {AttributeName} : System.Attribute + {{ + }} +}}"; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + // Add the marker attribute to the compilation. + context.RegisterPostInitializationOutput(ctx => ctx.AddSource( + "CssGeneratorAttribute.g.cs", + SourceText.From(AttributeSourceCode, Encoding.UTF8))); + + // Filter classes annotated with the [Report] attribute. Only filtered Syntax Nodes can trigger code generation. + var provider = context.SyntaxProvider + .CreateSyntaxProvider( + (s, _) => s is ClassDeclarationSyntax, + (ctx, _) => GetClassDeclarationForSourceGen(ctx)) + .Where(t => t.reportAttributeFound) + .Select((t, _) => t.Item1); + + // Generate the source code. + context.RegisterSourceOutput(context.CompilationProvider.Combine(provider.Collect()), + ((ctx, t) => GenerateCode(ctx, t.Left, t.Right))); + } + + /// + /// Checks whether the Node is annotated with the [Report] attribute and maps syntax context to the specific node type (ClassDeclarationSyntax). + /// + /// Syntax context, based on CreateSyntaxProvider predicate + /// The specific cast and whether the attribute was found. + private static (ClassDeclarationSyntax, bool reportAttributeFound) GetClassDeclarationForSourceGen( + GeneratorSyntaxContext context) + { + var classDeclarationSyntax = (ClassDeclarationSyntax)context.Node; + + // Go through all attributes of the class. + foreach (AttributeListSyntax attributeListSyntax in classDeclarationSyntax.AttributeLists) + foreach (AttributeSyntax attributeSyntax in attributeListSyntax.Attributes) + { + if (context.SemanticModel.GetSymbolInfo(attributeSyntax).Symbol is not IMethodSymbol attributeSymbol) + continue; // if we can't get the symbol, ignore it + + string attributeName = attributeSymbol.ContainingType.ToDisplayString(); + + // Check the full name of the [Report] attribute. + if (attributeName == $"{Namespace}.{AttributeName}") + return (classDeclarationSyntax, true); + } + + return (classDeclarationSyntax, false); + } + + /// + /// Generate code action. + /// It will be executed on specific nodes (ClassDeclarationSyntax annotated with the [Report] attribute) changed by the user. + /// + /// Source generation context used to add source files. + /// Compilation used to provide access to the Semantic Model. + /// Nodes annotated with the [Report] attribute that trigger the generate action. + private void GenerateCode(SourceProductionContext context, Compilation compilation, + ImmutableArray classDeclarations) + { + // Go through all filtered class declarations. + foreach (var classDeclarationSyntax in classDeclarations) + { + // We need to get semantic model of the class to retrieve metadata. + var semanticModel = compilation.GetSemanticModel(classDeclarationSyntax.SyntaxTree); + + // Symbols allow us to get the compile-time information. + if (semanticModel.GetDeclaredSymbol(classDeclarationSyntax) is not INamedTypeSymbol classSymbol) + continue; + + var namespaceName = classSymbol.ContainingNamespace.ToDisplayString(); + + // 'Identifier' means the token of the node. Get class name from the syntax node. + var className = classDeclarationSyntax.Identifier.Text; + + // Go through all class members with a particular type (property) to generate method lines. + var methodBody = classSymbol.GetMembers() + .OfType() + .Select(p => + $@" yield return $""{p.Name}:{{this.{p.Name}}}"";"); // e.g. yield return $"Id:{this.Id}"; + + // Build up the source code + var code = $$""" + // + + using System; + using System.Collections.Generic; + using Avalonia; + using Avalonia.Controls; + using Avalonia.Markup.Xaml; + using Avalonia.Media; + using Avalonia.Styling; + + namespace {{namespaceName}}; + + partial class {{className}} + { + public override void EndInit() //{{className}} + { + var headerBorderStyle = new Style(x => x.OfType().Class("header-border")); + headerBorderStyle.Setters.Add(new Setter(Border.BackgroundProperty, new SolidColorBrush(Colors.Lime, 0.5))); + headerBorderStyle.Setters.Add(new Setter(Border.BorderBrushProperty, new SolidColorBrush(Colors.Lime))); + headerBorderStyle.Setters.Add(new Setter(Border.BorderThicknessProperty, new Thickness(0.5))); + headerBorderStyle.Setters.Add(new Setter(Border.CornerRadiusProperty, new CornerRadius(5.0, 5.0, 0.0, 0.0))); + headerBorderStyle.Setters.Add(new Setter(Border.MaxWidthProperty, 450.0)); + headerBorderStyle.Setters.Add(new Setter(Decorator.PaddingProperty, new Thickness(10.0))); + + this.Styles.Add(headerBorderStyle); + + var headerTextStyle = new Style(x => x.OfType().Class("header")); + headerTextStyle.Setters.Add(new Setter(TextBlock.FontSizeProperty, 18.0)); + headerTextStyle.Setters.Add(new Setter(TextBlock.FontWeightProperty, FontWeight.Bold)); + + this.Styles.Add(headerTextStyle); + + var thinBorderStyle = new Style(x => x.OfType().Class("thin")); + thinBorderStyle.Setters.Add(new Setter(Border.BorderBrushProperty, new SolidColorBrush(Colors.Lime))); + thinBorderStyle.Setters.Add(new Setter(Border.BorderThicknessProperty, new Thickness(0.5))); + thinBorderStyle.Setters.Add(new Setter(Border.CornerRadiusProperty, new CornerRadius(0.0, 0.0, 5.0, 5.0))); + thinBorderStyle.Setters.Add(new Setter(Control.MarginProperty, new Thickness(0.0, 0.0, 0.0, 15.0))); + + this.Styles.Add(thinBorderStyle); + } + } + + """; + + // Add the source code to the compilation. + context.AddSource($"{className}.g.cs", SourceText.From(code, Encoding.UTF8)); + } + } +}