just test stuff

This commit is contained in:
Timothy Schenk 2024-08-07 19:06:55 +02:00
parent b5ac05a853
commit be0d5c08bf
Signed by: rainote
SSH key fingerprint: SHA256:pnkNSDwpAnaip00xaZlVFHKKsS7T8UtOomMzvs0yITE
18 changed files with 821 additions and 8 deletions

View file

@ -0,0 +1,3 @@
// Licensed to Timothy Schenk under the Apache 2.0 License.
Console.WriteLine("Hello, World!");

View file

@ -15,9 +15,9 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="FluentAssertions" Version="6.12.0" /> <PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="xunit" Version="2.7.0" /> <PackageReference Include="xunit" Version="2.9.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.7"> <PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

View file

@ -0,0 +1,11 @@
// Licensed to Timothy Schenk under the Apache 2.0 License.
namespace PacketMediator.Tests;
public class UnitTest1
{
[Fact]
public void Test1()
{
}
}

View file

@ -9,6 +9,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PacketMediator.Samples", "P
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PacketMediator.Tests", "PacketMediator.Tests\PacketMediator.Tests.csproj", "{C0508314-2B9C-4285-9EE5-6DED3DD8C2DA}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PacketMediator.Tests", "PacketMediator.Tests\PacketMediator.Tests.csproj", "{C0508314-2B9C-4285-9EE5-6DED3DD8C2DA}"
EndProject 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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{C0508314-2B9C-4285-9EE5-6DED3DD8C2DA}.Release|Any CPU.Build.0 = 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 EndGlobalSection
EndGlobal EndGlobal

View file

@ -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<string> GetAttributesInheriting(GeneratorSyntaxContext context)
{
var arrayOfInheritingTypes = new List<string>();
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<ICollection<string>> 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));
}
}

View file

@ -20,20 +20,26 @@
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
<IncludeSymbols>true</IncludeSymbols> <IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat> <SymbolPackageFormat>snupkg</SymbolPackageFormat>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<None Include="../README.md" Pack="true" PackagePath="\"/> <None Include="../README.md" Pack="true" PackagePath="\"/>
<PackageReference Include="DotNext" Version="5.3.1"/> <PackageReference Include="DotNext" Version="5.11.0" />
<PackageReference Include="DotNext.Metaprogramming" Version="5.3.0"/> <PackageReference Include="DotNext.Metaprogramming" Version="5.7.0" />
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0"/> <PackageReference Include="JetBrains.Annotations" Version="2024.2.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.146"> <PackageReference Include="Meziantou.Analyzer" Version="2.0.163">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0"/> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0"/>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1"/> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1"/>
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.9.28"> <PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.10.48">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

View file

@ -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<OperationCode>
{
public GamePacketIdAttribute(OperationCode code) : base(code)
{
}
}
public enum OperationCode : ushort
{
LoginRequest = 1
}

View file

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<RootNamespace>SourceGenerators1.Sample</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\SourceGenerators1\SourceGenerators1.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="true"/>
</ItemGroup>
<ItemGroup>
<None Remove="DDD.UbiquitousLanguageRegistry.txt"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2024.2.0" />
</ItemGroup>
</Project>

View file

@ -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 = @"// <auto-generated/>
using System;
using System.Collections.Generic;
namespace TestNamespace;
partial class Vector3
{
public IEnumerable<string> 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
);
}
}

View file

@ -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);
}
}

View file

@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<RootNamespace>SourceGenerators1.Tests</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing.XUnit" Version="1.1.1"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.2"/>
<PackageReference Include="xunit" Version="2.4.2"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SourceGenerators1\SourceGenerators1.csproj"/>
</ItemGroup>
</Project>

View file

@ -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; }
}

View file

@ -0,0 +1,9 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"DebugRoslynSourceGenerator": {
"commandName": "DebugRoslynComponent",
"targetProject": "../SourceGenerators1.Sample/SourceGenerators1.Sample.csproj"
}
}
}

View file

@ -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: [Lets 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).

View file

@ -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;
/// <summary>
/// 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.
/// </summary>
[Generator]
public class SampleIncrementalSourceGenerator : IIncrementalGenerator
{
private const string Namespace = "Generators";
private const string AttributeName = "ReportAttribute";
private const string AttributeSourceCode = $@"// <auto-generated/>
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))
);
}
/// <summary>
/// Checks whether the Node is annotated with the [Report] attribute and maps syntax context to the specific node type (ClassDeclarationSyntax).
/// </summary>
/// <param name="context">Syntax context, based on CreateSyntaxProvider predicate</param>
/// <returns>The specific cast and whether the attribute was found.</returns>
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);
}
/// <summary>
/// Generate code action.
/// It will be executed on specific nodes (ClassDeclarationSyntax annotated with the [Report] attribute) changed by the user.
/// </summary>
/// <param name="context">Source generation context used to add source files.</param>
/// <param name="compilation">Compilation used to provide access to the Semantic Model.</param>
/// <param name="classDeclarations">Nodes annotated with the [Report] attribute that trigger the generate action.</param>
private void GenerateCode(SourceProductionContext context, Compilation compilation,
ImmutableArray<ClassDeclarationSyntax> 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<IPropertySymbol>()
.Select(p =>
$@" yield return $""{p.Name}:{{this.{p.Name}}}"";"
); // e.g. yield return $"Id:{this.Id}";
// Build up the source code
var code = $@"// <auto-generated/>
using System;
using System.Collections.Generic;
namespace {namespaceName};
partial class {className}
{{
public IEnumerable<string> Report()
{{
{string.Join("\n", methodBody)}
}}
}}
";
// Add the source code to the compilation.
context.AddSource($"{className}.g.cs", SourceText.From(code, Encoding.UTF8));
}
}
}

View file

@ -0,0 +1,56 @@
using System.IO;
using Microsoft.CodeAnalysis;
namespace SourceGenerators1;
/// <summary>
/// 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.
/// </summary>
[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 = $@"// <auto-generated/>
namespace Entities
{{
public partial class {className}
{{
}}
}}
";
// Add the source code to the compilation.
context.AddSource($"{className}.g.cs", source);
}
}
}
}

View file

@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<IsRoslynComponent>true</IsRoslynComponent>
<RootNamespace>SourceGenerators1</RootNamespace>
<PackageId>SourceGenerators1</PackageId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2024.2.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.0"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.3.0"/>
</ItemGroup>
<ItemGroup>
<Compile Remove="test.snippet.cs" />
</ItemGroup>
</Project>

View file

@ -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<BaseTypeSyntax>())
{
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<BaseTypeSyntax>())
{
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<AttributeClassMapping?> Right) tuple)
{
var dependencies = new string[] { "System", "System.Threading.Channels", "SourceGenerators1.Sample" };
/*
var lambda = CodeGenerator.Lambda<Func<byte[], IIncomingPacket>>(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 = $$"""
//<auto-generated/>
{{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<TPacketIdEnum> : 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;