diff --git a/PacketMediator.Samples/Program.cs b/PacketMediator.Samples/Program.cs new file mode 100644 index 0000000..cf121b5 --- /dev/null +++ b/PacketMediator.Samples/Program.cs @@ -0,0 +1,3 @@ +// Licensed to Timothy Schenk under the Apache 2.0 License. + +Console.WriteLine("Hello, World!"); diff --git a/PacketMediator.Tests/PacketMediator.Tests.csproj b/PacketMediator.Tests/PacketMediator.Tests.csproj index 2a25ccb..0ec6f08 100644 --- a/PacketMediator.Tests/PacketMediator.Tests.csproj +++ b/PacketMediator.Tests/PacketMediator.Tests.csproj @@ -15,9 +15,9 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/PacketMediator.Tests/UnitTest1.cs b/PacketMediator.Tests/UnitTest1.cs new file mode 100644 index 0000000..f6d6791 --- /dev/null +++ b/PacketMediator.Tests/UnitTest1.cs @@ -0,0 +1,11 @@ +// Licensed to Timothy Schenk under the Apache 2.0 License. + +namespace PacketMediator.Tests; + +public class UnitTest1 +{ + [Fact] + public void Test1() + { + } +} diff --git a/PacketMediator.sln b/PacketMediator.sln index 2542cb5..68a3d38 100644 --- a/PacketMediator.sln +++ b/PacketMediator.sln @@ -9,6 +9,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PacketMediator.Samples", "P EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PacketMediator.Tests", "PacketMediator.Tests\PacketMediator.Tests.csproj", "{C0508314-2B9C-4285-9EE5-6DED3DD8C2DA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SourceGenerators1", "SourceGenerators1\SourceGenerators1\SourceGenerators1.csproj", "{268AA6B4-0EBD-483B-A147-6B8D0E5B9F92}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SourceGenerators1.Sample", "SourceGenerators1\SourceGenerators1.Sample\SourceGenerators1.Sample.csproj", "{AA60BE32-4D65-4657-8ACF-ACF1437E2D1A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SourceGenerators1.Tests", "SourceGenerators1\SourceGenerators1.Tests\SourceGenerators1.Tests.csproj", "{60921557-6F31-43D7-A0CE-ED5C6E7DCB57}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -30,5 +36,17 @@ Global {C0508314-2B9C-4285-9EE5-6DED3DD8C2DA}.Debug|Any CPU.Build.0 = Debug|Any CPU {C0508314-2B9C-4285-9EE5-6DED3DD8C2DA}.Release|Any CPU.ActiveCfg = Release|Any CPU {C0508314-2B9C-4285-9EE5-6DED3DD8C2DA}.Release|Any CPU.Build.0 = Release|Any CPU + {268AA6B4-0EBD-483B-A147-6B8D0E5B9F92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {268AA6B4-0EBD-483B-A147-6B8D0E5B9F92}.Debug|Any CPU.Build.0 = Debug|Any CPU + {268AA6B4-0EBD-483B-A147-6B8D0E5B9F92}.Release|Any CPU.ActiveCfg = Release|Any CPU + {268AA6B4-0EBD-483B-A147-6B8D0E5B9F92}.Release|Any CPU.Build.0 = Release|Any CPU + {AA60BE32-4D65-4657-8ACF-ACF1437E2D1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AA60BE32-4D65-4657-8ACF-ACF1437E2D1A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AA60BE32-4D65-4657-8ACF-ACF1437E2D1A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AA60BE32-4D65-4657-8ACF-ACF1437E2D1A}.Release|Any CPU.Build.0 = Release|Any CPU + {60921557-6F31-43D7-A0CE-ED5C6E7DCB57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {60921557-6F31-43D7-A0CE-ED5C6E7DCB57}.Debug|Any CPU.Build.0 = Debug|Any CPU + {60921557-6F31-43D7-A0CE-ED5C6E7DCB57}.Release|Any CPU.ActiveCfg = Release|Any CPU + {60921557-6F31-43D7-A0CE-ED5C6E7DCB57}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/RaiNote.PacketMediator/PacketHandlerGenerator.cs b/RaiNote.PacketMediator/PacketHandlerGenerator.cs new file mode 100644 index 0000000..f468967 --- /dev/null +++ b/RaiNote.PacketMediator/PacketHandlerGenerator.cs @@ -0,0 +1,66 @@ +using System.CodeDom.Compiler; +using System.Collections.Immutable; +using System.Globalization; +using System.Reflection; +using System.Resources; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace RaiNote.PacketMediator; + +[Generator] +public class PacketHandlerGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var structProvider = context.SyntaxProvider.CreateSyntaxProvider( + predicate: static (node, _) => node is StructDeclarationSyntax, + transform: static (ctx, _) => GetAttributesInheriting(ctx) + ) + .Where(m => m is not null); + var classProvider = context.SyntaxProvider.CreateSyntaxProvider( + predicate: static (node, _) => node is ClassDeclarationSyntax, + transform: static (ctx, _) => ctx.Node as ClassDeclarationSyntax + ) + .Where(m => m is not null); + var compilation = context.CompilationProvider.Combine(structProvider.Collect()); + context.RegisterSourceOutput(compilation, Execute); + var writer = new IndentedTextWriter(new StringWriter()); + } + + private static ICollection GetAttributesInheriting(GeneratorSyntaxContext context) + { + var arrayOfInheritingTypes = new List(); + var structDeclarationSyntax = (StructDeclarationSyntax)context.Node; + foreach (var attributeList in structDeclarationSyntax.AttributeLists) + { + foreach (var attribute in attributeList.Attributes) + { + if (context.SemanticModel.GetSymbolInfo(attribute).Symbol is not IMethodSymbol attributeSymbol) + continue; // if we can't get the symbol, ignore it + if (context.SemanticModel.GetSymbolInfo(attribute).Symbol is not ITypeSymbol attributeTypeSymbol) + continue; + if (!attributeTypeSymbol.AllInterfaces + .Select(x => x.Name.Equals(typeof(PacketIdAttribute<>).ToString(), StringComparison.Ordinal)) + .Any()) + continue; + + arrayOfInheritingTypes.Add(attributeTypeSymbol.Name); + } + } + + return arrayOfInheritingTypes; + } + + private void Execute(SourceProductionContext context, + (Compilation Left, ImmutableArray> Right) valueTuple) + { + var (compilation, attributeNames) = valueTuple; + var code = $@"{string.Join('\n', attributeNames)}"; + code += "\r\n"; + + context.AddSource("Test.g.cs", SourceText.From(code, Encoding.UTF8)); + } +} diff --git a/RaiNote.PacketMediator/RaiNote.PacketMediator.csproj b/RaiNote.PacketMediator/RaiNote.PacketMediator.csproj index fc6427c..a8b4d55 100644 --- a/RaiNote.PacketMediator/RaiNote.PacketMediator.csproj +++ b/RaiNote.PacketMediator/RaiNote.PacketMediator.csproj @@ -20,20 +20,26 @@ README.md true snupkg + true - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/SourceGenerators1/SourceGenerators1.Sample/Examples.cs b/SourceGenerators1/SourceGenerators1.Sample/Examples.cs new file mode 100644 index 0000000..131971e --- /dev/null +++ b/SourceGenerators1/SourceGenerators1.Sample/Examples.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Text; +using JetBrains.Annotations; + +namespace SourceGenerators1.Sample; + +[GamePacketIdAttribute(OperationCode.LoginRequest)] +public class LoginRequestPacket : IIncomingPacket +{ + public string Username { get; set; } + + public string Password { get; set; } + + public void Deserialize(byte[] data) + { + Username = Encoding.ASCII.GetString(data, 0, 32); + Password = Encoding.ASCII.GetString(data, 32, 64); + } +} + +// GamePacketIdAttribute.cs +[AttributeUsage(AttributeTargets.Class, Inherited = false)] +public class GamePacketIdAttribute : PacketIdAttribute +{ + public GamePacketIdAttribute(OperationCode code) : base(code) + { + } +} + +public enum OperationCode : ushort +{ + LoginRequest = 1 +} + diff --git a/SourceGenerators1/SourceGenerators1.Sample/SourceGenerators1.Sample.csproj b/SourceGenerators1/SourceGenerators1.Sample/SourceGenerators1.Sample.csproj new file mode 100644 index 0000000..2b388ba --- /dev/null +++ b/SourceGenerators1/SourceGenerators1.Sample/SourceGenerators1.Sample.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + enable + SourceGenerators1.Sample + + + + + + + + + + + + + + + diff --git a/SourceGenerators1/SourceGenerators1.Tests/SampleIncrementalSourceGeneratorTests.cs b/SourceGenerators1/SourceGenerators1.Tests/SampleIncrementalSourceGeneratorTests.cs new file mode 100644 index 0000000..7ac24dd --- /dev/null +++ b/SourceGenerators1/SourceGenerators1.Tests/SampleIncrementalSourceGeneratorTests.cs @@ -0,0 +1,70 @@ +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Xunit; + +namespace SourceGenerators1.Tests; + +public class SampleIncrementalSourceGeneratorTests +{ + 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 SampleIncrementalSourceGenerator(); + + // 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(SampleSourceGeneratorTests), + 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/SourceGenerators1/SourceGenerators1.Tests/SampleSourceGeneratorTests.cs b/SourceGenerators1/SourceGenerators1.Tests/SampleSourceGeneratorTests.cs new file mode 100644 index 0000000..9d272f6 --- /dev/null +++ b/SourceGenerators1/SourceGenerators1.Tests/SampleSourceGeneratorTests.cs @@ -0,0 +1,44 @@ +using System.IO; +using System.Linq; +using SourceGenerators1.Tests.Utils; +using Microsoft.CodeAnalysis.CSharp; +using Xunit; + +namespace SourceGenerators1.Tests; + +public class SampleSourceGeneratorTests +{ + private const string DddRegistryText = @"User +Document +Customer"; + + [Fact] + public void GenerateClassesBasedOnDDDRegistry() + { + // Create an instance of the source generator. + var generator = new SampleSourceGenerator(); + + // Source generators should be tested using 'GeneratorDriver'. + var driver = CSharpGeneratorDriver.Create(new[] { generator }, + new[] + { + // Add the additional file separately from the compilation. + new TestAdditionalFile("./DDD.UbiquitousLanguageRegistry.txt", DddRegistryText) + } + ); + + // To run generators, we can use an empty compilation. + var compilation = CSharpCompilation.Create(nameof(SampleSourceGeneratorTests)); + + // 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/SourceGenerators1/SourceGenerators1.Tests/SourceGenerators1.Tests.csproj b/SourceGenerators1/SourceGenerators1.Tests/SourceGenerators1.Tests.csproj new file mode 100644 index 0000000..abb5fc2 --- /dev/null +++ b/SourceGenerators1/SourceGenerators1.Tests/SourceGenerators1.Tests.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + + false + + SourceGenerators1.Tests + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/SourceGenerators1/SourceGenerators1.Tests/Utils/TestAdditionalFile.cs b/SourceGenerators1/SourceGenerators1.Tests/Utils/TestAdditionalFile.cs new file mode 100644 index 0000000..40297c4 --- /dev/null +++ b/SourceGenerators1/SourceGenerators1.Tests/Utils/TestAdditionalFile.cs @@ -0,0 +1,20 @@ +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace SourceGenerators1.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/SourceGenerators1/SourceGenerators1/Properties/launchSettings.json b/SourceGenerators1/SourceGenerators1/Properties/launchSettings.json new file mode 100644 index 0000000..7254224 --- /dev/null +++ b/SourceGenerators1/SourceGenerators1/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "DebugRoslynSourceGenerator": { + "commandName": "DebugRoslynComponent", + "targetProject": "../SourceGenerators1.Sample/SourceGenerators1.Sample.csproj" + } + } +} \ No newline at end of file diff --git a/SourceGenerators1/SourceGenerators1/Readme.md b/SourceGenerators1/SourceGenerators1/Readme.md new file mode 100644 index 0000000..cc30b4f --- /dev/null +++ b/SourceGenerators1/SourceGenerators1/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 +### SourceGenerators1 +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. + +### SourceGenerators1.Sample +A project that references source generators. Note the parameters of `ProjectReference` in [SourceGenerators1.Sample.csproj](../SourceGenerators1.Sample/SourceGenerators1.Sample.csproj), they make sure that the project is referenced as a set of source generators. + +### SourceGenerators1.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/SourceGenerators1/SourceGenerators1/SampleIncrementalSourceGenerator.cs b/SourceGenerators1/SourceGenerators1/SampleIncrementalSourceGenerator.cs new file mode 100644 index 0000000..6e92121 --- /dev/null +++ b/SourceGenerators1/SourceGenerators1/SampleIncrementalSourceGenerator.cs @@ -0,0 +1,135 @@ +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + + +namespace SourceGenerators1; + +/// +/// 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 SampleIncrementalSourceGenerator : IIncrementalGenerator +{ + private const string Namespace = "Generators"; + private const string AttributeName = "ReportAttribute"; + + 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( + "ReportAttribute.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; + +namespace {namespaceName}; + +partial class {className} +{{ + public IEnumerable Report() + {{ +{string.Join("\n", methodBody)} + }} +}} +"; + + // Add the source code to the compilation. + context.AddSource($"{className}.g.cs", SourceText.From(code, Encoding.UTF8)); + } + } +} diff --git a/SourceGenerators1/SourceGenerators1/SampleSourceGenerator.cs b/SourceGenerators1/SourceGenerators1/SampleSourceGenerator.cs new file mode 100644 index 0000000..d70106f --- /dev/null +++ b/SourceGenerators1/SourceGenerators1/SampleSourceGenerator.cs @@ -0,0 +1,56 @@ +using System.IO; +using Microsoft.CodeAnalysis; + +namespace SourceGenerators1; + +/// +/// 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 SampleSourceGenerator : ISourceGenerator +{ + public void Initialize(GeneratorInitializationContext context) + { + // No initialization required for this generator. + } + + public void Execute(GeneratorExecutionContext context) + { + // If you would like to put some data to non-compilable file (e.g. a .txt file), mark it as an Additional File. + + // Go through all files marked as an Additional File in file properties. + foreach (var additionalFile in context.AdditionalFiles) + { + if (additionalFile == null) + continue; + + // Check if the file name is the specific file that we expect. + if (Path.GetFileName(additionalFile.Path) != "DDD.UbiquitousLanguageRegistry.txt") + continue; + + var text = additionalFile.GetText(); + if (text == null) + continue; + + foreach (var line in text.Lines) + { + var className = line.ToString().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/SourceGenerators1/SourceGenerators1/SourceGenerators1.csproj b/SourceGenerators1/SourceGenerators1/SourceGenerators1.csproj new file mode 100644 index 0000000..99cd737 --- /dev/null +++ b/SourceGenerators1/SourceGenerators1/SourceGenerators1.csproj @@ -0,0 +1,31 @@ + + + + netstandard2.1 + false + enable + latest + + true + true + + SourceGenerators1 + SourceGenerators1 + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + diff --git a/SourceGenerators1/SourceGenerators1/Test.cs b/SourceGenerators1/SourceGenerators1/Test.cs new file mode 100644 index 0000000..6edc886 --- /dev/null +++ b/SourceGenerators1/SourceGenerators1/Test.cs @@ -0,0 +1,232 @@ +using System; +using System.CodeDom.Compiler; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using JetBrains.Annotations; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace SourceGenerators1; + +[Generator] +public class PacketHandlerGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var attributeProvider = context.SyntaxProvider.CreateSyntaxProvider( + predicate: static (node, _) => node is ClassDeclarationSyntax, + transform: static (ctx, _) => GetAttributesInheriting(ctx) + ) + .Where(m => m is not null); + + var attribute = attributeProvider.Select((data, _) => data); + + /* + var classProvider = context.SyntaxProvider.CreateSyntaxProvider( + predicate: static (node, _) => node is ClassDeclarationSyntax, + transform: (ctx, _) => GetIncomingPacketTypes(ctx, attribute) + ) + .Where(m => m is not null); + + var test = attributeProvider.Combine(classProvider.Collect());*/ + + context.RegisterSourceOutput(context.CompilationProvider.Combine(attributeProvider.Collect()), + Execute + ); + } + + private static string? GetIncomingPacketTypes(GeneratorSyntaxContext context, string? desiredAttributeName) + { + var structDeclarationSyntax = (ClassDeclarationSyntax)context.Node; + + var isValidBaseType = false; + foreach (var baseTypeSyntax in structDeclarationSyntax.BaseList?.Types ?? Enumerable.Empty()) + { + var simpleBaseTypeSyntax = baseTypeSyntax as SimpleBaseTypeSyntax; + var genericName = simpleBaseTypeSyntax?.Type as GenericNameSyntax; + if (genericName?.Identifier.ToString() != + GetAttributeName(typeof(IIncomingPacket))) + { + continue; + } + + isValidBaseType = true; + } + + if (!isValidBaseType) + { + return null; + } + + foreach (var attributeList in structDeclarationSyntax.AttributeLists) + { + foreach (var attributeSyntax in attributeList.Attributes) + { + if (attributeSyntax.Name.ToString() != desiredAttributeName) + continue; + + return structDeclarationSyntax.Identifier.Text; + } + } + + return null; + } + + private static AttributeClassMapping? GetAttributesInheriting(GeneratorSyntaxContext context) + { + var structDeclarationSyntax = (ClassDeclarationSyntax)context.Node; + var targetName = structDeclarationSyntax.Identifier.Text; + string? attributeName = null; + var isValidAttribute = false; + var isValidPacketType = false; + foreach (var attributeList in structDeclarationSyntax.AttributeLists) + { + foreach (var attributeSyntax in attributeList.Attributes) + { + } + } + foreach (var baseTypeSyntax in structDeclarationSyntax.BaseList?.Types ?? Enumerable.Empty()) + { + var simpleBaseTypeSyntax = baseTypeSyntax as SimpleBaseTypeSyntax; + var genericName = simpleBaseTypeSyntax?.Type as GenericNameSyntax; + if (genericName?.Identifier.ToString() == + GetAttributeName(typeof(PacketIdAttribute<>))) + { + isValidAttribute = true; + attributeName = simpleBaseTypeSyntax?.ToFullString(); + } + var interfaceName = simpleBaseTypeSyntax?.Type as IdentifierNameSyntax; + + if (interfaceName?.Identifier.ToString() == nameof(IIncomingPacket)) + isValidPacketType = true; + } + + if (isValidPacketType) + return new AttributeClassMapping(targetName, attributeName); + + return null; + } + + private struct AttributeClassMapping + { + public AttributeClassMapping(string? className, string? attributeName) + { + this.ClassName = className; + this.AttributeName = attributeName; + } + + public readonly string? ClassName; + public readonly string? AttributeName; + } + + private static string GetAttributeName(Type type) + { + return type.IsGenericType ? type.Name.Split('`')[0] : type.Name; + } + + private static void Execute(SourceProductionContext context, + (Compilation Left, ImmutableArray Right) tuple) + { + var dependencies = new string[] { "System", "System.Threading.Channels", "SourceGenerators1.Sample" }; + /* + var lambda = CodeGenerator.Lambda>(fun => + { + var argPacketData = fun[0]; + var newPacket = packetsType.Value.New(); + + var packetVariable = CodeGenerator.DeclareVariable(packetsType.Value, "packet"); + CodeGenerator.Assign(packetVariable, newPacket); + CodeGenerator.Call(packetVariable, nameof(IIncomingPacket.Deserialize), argPacketData); + + CodeGenerator.Return(packetVariable); + }).Compile(); + */ + var allparsingFunctions = ""; + context.ReportDiagnostic(Diagnostic.Create( + new DiagnosticDescriptor("fuckyou0001", + "FuckYou", + "Message: {0}", + "FUCK YOU", + DiagnosticSeverity.Error, + true + ), + Location.None, + string.Join(", ", + tuple.Right.Select(x => + { + if (!x.HasValue) + return string.Empty; + return x.Value.AttributeName + ":" + x.Value.ClassName; + } + ) + ) + ) + ); + if (tuple.Right.Length == 0) + { + throw new Exception(); + } + + foreach (var packetName in tuple.Right) + { + var parsingFunctionTemplate = $$""" + public {{packetName?.ClassName}} Parse(byte[] data) { + var packet = new {{packetName?.ClassName}}(); + packet.Deserialize(data); + return packet; + } + """; + allparsingFunctions += parsingFunctionTemplate; + } + + var codeTemplate = $$""" + // + {{string.Join('\n', dependencies.Select(dep => $"using {dep};"))}} + + public class PacketDistributorService{ + + {{allparsingFunctions}} + + private async Task InvokePacketHandlerAsync((byte[], TPacketIdEnum, TSession) valueTuple, + CancellationToken cancellationToken) + { + var (packetData, operationCode, session) = valueTuple; + if (!_deserializationMap.TryGetValue(operationCode, out var func)) + { + return; + } + + var packet = func(packetData); + + await _packetHandlersInstantiation[operationCode]?.TryHandleAsync(packet, session, cancellationToken)!; + } + } + """; + context.AddSource("Test.g.cs", SourceText.From(codeTemplate, Encoding.UTF8)); + } +} + +[AttributeUsage(AttributeTargets.Class, Inherited = false)] +public abstract class PacketIdAttribute : Attribute where TPacketIdEnum : Enum +{ + protected PacketIdAttribute(TPacketIdEnum code) + { + Code = code; + } + + public TPacketIdEnum Code { get; } +} + +[UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)] +public interface IIncomingPacket : IPacket +{ + [UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)] + public void Deserialize(byte[] data); +} + +public interface IPacket;