From c81b83295fff46b4d0f2b7a222e1888a39361333 Mon Sep 17 00:00:00 2001 From: Maxim Tkachenko Date: Sun, 5 Oct 2025 19:31:19 +0200 Subject: [PATCH 1/7] updated projects to dotnet9 + switched to incremental source generator --- .gitignore | 3 + benchmark/Parsers.Benchmarks.csproj | 4 +- .../ParserSourceGenerator.cs | 64 +++++++++++-------- .../Parsers.SourceGenerator.csproj | 8 ++- .../Properties/launchSettings.json | 9 +++ src/Parsers/Parsers.csproj | 4 +- test/Parsers.Tests.csproj | 12 ++-- 7 files changed, 67 insertions(+), 37 deletions(-) create mode 100644 src/Parsers.SourceGenerator/Properties/launchSettings.json diff --git a/.gitignore b/.gitignore index dfcfd56..e21c381 100644 --- a/.gitignore +++ b/.gitignore @@ -348,3 +348,6 @@ MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ + +.idea +.DS_Store diff --git a/benchmark/Parsers.Benchmarks.csproj b/benchmark/Parsers.Benchmarks.csproj index 78bf7ea..c583946 100644 --- a/benchmark/Parsers.Benchmarks.csproj +++ b/benchmark/Parsers.Benchmarks.csproj @@ -2,11 +2,11 @@ Exe - net5.0 + net9.0 - + diff --git a/src/Parsers.SourceGenerator/ParserSourceGenerator.cs b/src/Parsers.SourceGenerator/ParserSourceGenerator.cs index 3df9a5f..5339e13 100644 --- a/src/Parsers.SourceGenerator/ParserSourceGenerator.cs +++ b/src/Parsers.SourceGenerator/ParserSourceGenerator.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Text; using Microsoft.CodeAnalysis; @@ -10,35 +11,48 @@ namespace Parsers.SourceGenerator /// /// http://dontcodetired.com/blog/post/C-Source-Generators-Less-Boilerplate-Code-More-Productivity /// https://github.com/amis92/csharp-source-generators + /// + /// https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.cookbook.md + /// https://blog.jetbrains.com/dotnet/2023/07/13/debug-source-generators-in-jetbrains-rider/ + /// https://andrewlock.net/exploring-dotnet-6-part-9-source-generator-updates-incremental-generators/#creating-a-source-generator-with-the-loggermessage-source-generator- /// [Generator] - public class ParserSourceGenerator : ISourceGenerator + public class ParserSourceGenerator : IIncrementalGenerator { - public void Initialize(GeneratorInitializationContext context) + public void Initialize(IncrementalGeneratorInitializationContext context) { - //uncomment to debug - //System.Diagnostics.Debugger.Launch(); + var typesToParse = context.SyntaxProvider + .CreateSyntaxProvider( + predicate: static (syntaxNode, _) => syntaxNode is ClassDeclarationSyntax { AttributeLists.Count: > 0 }, + transform: (ctx, _) => + { + var clasDeclaration = (ClassDeclarationSyntax)ctx.Node; + + var parserOutputTypeSymbol = ctx.SemanticModel.Compilation + .GetTypeByMetadataName("Parsers.ParserOutputAttribute"); + + var symbol = ctx.SemanticModel.GetDeclaredSymbol(clasDeclaration) as INamedTypeSymbol; + if (symbol == null) return null; + + return symbol.GetAttributes().Any(a => + SymbolEqualityComparer.Default.Equals(a.AttributeClass, parserOutputTypeSymbol)) + ? symbol + : null; + }) + .Where(static m => m is not null) + .Collect(); + + context.RegisterSourceOutput( + context.CompilationProvider.Combine(typesToParse), + (k, f ) => + { + var attributeIndexTypeSymbol = f.Left.GetTypeByMetadataName("Parsers.ArrayIndexAttribute"); + GenerateSerializer(k, f.Right, attributeIndexTypeSymbol); + }); } - - public void Execute(GeneratorExecutionContext context) + + private static void GenerateSerializer(SourceProductionContext context, ImmutableArray classSymbol, INamedTypeSymbol attributeIndexTypeSymbol) { - var compilation = context.Compilation; - var parserOutputTypeSymbol = compilation.GetTypeByMetadataName("Parsers.ParserOutputAttribute"); - var attributeIndexTypeSymbol = compilation.GetTypeByMetadataName("Parsers.ArrayIndexAttribute"); - var typesToParse = new List(); - - foreach (var syntaxTree in compilation.SyntaxTrees) - { - var semanticModel = compilation.GetSemanticModel(syntaxTree); - typesToParse.AddRange(syntaxTree.GetRoot() - .DescendantNodesAndSelf() - .OfType() - .Select(x => semanticModel.GetDeclaredSymbol(x)) - .OfType() - .Where(x => x.GetAttributes().Select(a => a.AttributeClass) - .Any(b => b == parserOutputTypeSymbol))); - } - var typeNames = new List<(string TargetTypeName, string TargetTypeFullName, string TargetTypeParserName)>(); var builder = new StringBuilder(); builder.AppendLine(@" @@ -48,7 +62,7 @@ namespace BySourceGenerator { public class Parser : IParserFactory {"); - foreach (var typeSymbol in typesToParse) + foreach (var typeSymbol in classSymbol.OfType()) { var targetTypeName = typeSymbol.Name; var targetTypeFullName = GetFullName(typeSymbol); @@ -64,7 +78,7 @@ public class Parser : IParserFactory foreach (var prop in props) { var attr = prop.GetAttributes().FirstOrDefault(x => x.AttributeClass == attributeIndexTypeSymbol); - if (attr == null || !(attr.ConstructorArguments[0].Value is int)) continue; + if (attr == null || attr.ConstructorArguments[0].Value is not int) continue; int order = (int) attr.ConstructorArguments[0].Value; if (order < 0) continue; diff --git a/src/Parsers.SourceGenerator/Parsers.SourceGenerator.csproj b/src/Parsers.SourceGenerator/Parsers.SourceGenerator.csproj index d1ef46c..ea13059 100644 --- a/src/Parsers.SourceGenerator/Parsers.SourceGenerator.csproj +++ b/src/Parsers.SourceGenerator/Parsers.SourceGenerator.csproj @@ -2,14 +2,18 @@ netstandard2.0 + true + true + true + latest - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/Parsers.SourceGenerator/Properties/launchSettings.json b/src/Parsers.SourceGenerator/Properties/launchSettings.json new file mode 100644 index 0000000..8353097 --- /dev/null +++ b/src/Parsers.SourceGenerator/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "Generators": { + "commandName": "DebugRoslynComponent", + "targetProject": "../../test/Parsers.Tests.csproj" + } + } +} \ No newline at end of file diff --git a/src/Parsers/Parsers.csproj b/src/Parsers/Parsers.csproj index 8b3f486..e576515 100644 --- a/src/Parsers/Parsers.csproj +++ b/src/Parsers/Parsers.csproj @@ -1,11 +1,11 @@  - net5.0 + net9.0 - + diff --git a/test/Parsers.Tests.csproj b/test/Parsers.Tests.csproj index ae1ba5b..6d6495b 100644 --- a/test/Parsers.Tests.csproj +++ b/test/Parsers.Tests.csproj @@ -1,19 +1,19 @@  - net5.0 + net9.0 false - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive From 764adcc2e1cb44b999bd3630d0241ff8599594ac Mon Sep 17 00:00:00 2001 From: Maxim Tkachenko Date: Sun, 5 Oct 2025 19:33:35 +0200 Subject: [PATCH 2/7] updated the workflow to dotnet9 --- .github/workflows/dotnet.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 659168f..773e424 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -16,7 +16,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: 5.0.x + dotnet-version: 9.0.x - name: Restore dependencies run: dotnet restore - name: Build From 02644d860575b5558cc97b989ab57db81225765a Mon Sep 17 00:00:00 2001 From: Maxim Tkachenko Date: Sun, 5 Oct 2025 19:44:41 +0200 Subject: [PATCH 3/7] use non-commercial version of fluent assertions --- test/Parsers.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Parsers.Tests.csproj b/test/Parsers.Tests.csproj index 6d6495b..404059e 100644 --- a/test/Parsers.Tests.csproj +++ b/test/Parsers.Tests.csproj @@ -6,7 +6,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive From a250e32d6ca6f1d34c5b8a3930459536e603065e Mon Sep 17 00:00:00 2001 From: Maxim Tkachenko Date: Sun, 5 Oct 2025 19:44:54 +0200 Subject: [PATCH 4/7] improved tests --- test/ParserGeneratorTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ParserGeneratorTests.cs b/test/ParserGeneratorTests.cs index 3aced8a..18fed7a 100644 --- a/test/ParserGeneratorTests.cs +++ b/test/ParserGeneratorTests.cs @@ -44,7 +44,7 @@ public void GetParserUsingArrayIndex_InvalidInput_DefaultValuesNoException(IPars new object[] { new SigilParserFactory() }, new object[] { new CachedParserFactory(new SigilParserFactory()) }, new object[] { RoslynParserInitializer.CreateFactory() }, - new object[] { (IParserFactory)Activator.CreateInstance(Type.GetType("BySourceGenerator.Parser")) }, + new object[] { new BySourceGenerator.Parser() }, new object[] { new ReflectionParserFactory() }, }; } From a3dd2e7b996531ac963e05f37e8221d14e65cd03 Mon Sep 17 00:00:00 2001 From: Maxim Tkachenko Date: Mon, 13 Oct 2025 17:55:18 +0200 Subject: [PATCH 5/7] more improvements --- benchmark/Data.cs | 23 +- benchmark/GetParser_Benchmark.cs | 71 +++-- benchmark/ParserInvocation_Benchmark.cs | 135 +++++----- benchmark/Program.cs | 41 +-- .../ParserSourceGenerator.cs | 251 ++++++++++-------- .../Properties/launchSettings.json | 2 +- src/Parsers/Attributes.cs | 22 +- src/Parsers/CachedParserFactory.cs | 26 +- src/Parsers/EmitIlParserFactory.cs | 101 ++++--- src/Parsers/ExpressionTreeParserFactory.cs | 93 ++++--- src/Parsers/IParserFactory.cs | 11 +- src/Parsers/ReflectionParserFactory.cs | 67 +++-- src/Parsers/RoslynParserInitializer.cs | 173 ++++++------ src/Parsers/SigilParserFactory.cs | 97 ++++--- src/Parsers/TypeParsers.cs | 17 +- test/Data.cs | 27 +- test/ParserGeneratorTests.cs | 85 +++--- 17 files changed, 610 insertions(+), 632 deletions(-) diff --git a/benchmark/Data.cs b/benchmark/Data.cs index 72674b3..6565953 100644 --- a/benchmark/Data.cs +++ b/benchmark/Data.cs @@ -1,17 +1,16 @@ using System; -namespace Parsers.Benchmarks +namespace Parsers.Benchmarks; + +[ParserOutput] +public class Data { - [ParserOutput] - public class Data - { - [ArrayIndex(0)] - public string Name { get; set; } + [ArrayIndex(0)] + public string Name { get; set; } - [ArrayIndex(2)] - public int Number { get; set; } + [ArrayIndex(2)] + public int Number { get; set; } - [ArrayIndex(1)] - public DateTime Birthday { get; set; } - } -} + [ArrayIndex(1)] + public DateTime Birthday { get; set; } +} \ No newline at end of file diff --git a/benchmark/GetParser_Benchmark.cs b/benchmark/GetParser_Benchmark.cs index ab03e5a..4398de0 100644 --- a/benchmark/GetParser_Benchmark.cs +++ b/benchmark/GetParser_Benchmark.cs @@ -1,46 +1,45 @@ using System; using BenchmarkDotNet.Attributes; -namespace Parsers.Benchmarks +namespace Parsers.Benchmarks; + +// ReSharper disable once InconsistentNaming +[MemoryDiagnoser] +public class GetParser_Benchmark { - // ReSharper disable once InconsistentNaming - [MemoryDiagnoser] - public class GetParser_Benchmark - { - private EmitIlParserFactory _emitIlParserFactory; - private ExpressionTreeParserFactory _expressionTreeParserFactory; - private SigilParserFactory _sigilParserFactory; + private EmitIlParserFactory _emitIlParserFactory; + private ExpressionTreeParserFactory _expressionTreeParserFactory; + private SigilParserFactory _sigilParserFactory; - [GlobalSetup] - public void GlobalSetup() - { - _emitIlParserFactory = new EmitIlParserFactory(); - _expressionTreeParserFactory = new ExpressionTreeParserFactory(); - _sigilParserFactory = new SigilParserFactory(); - } + [GlobalSetup] + public void GlobalSetup() + { + _emitIlParserFactory = new EmitIlParserFactory(); + _expressionTreeParserFactory = new ExpressionTreeParserFactory(); + _sigilParserFactory = new SigilParserFactory(); + } - [Benchmark] - public Func EmitIl() - { - return _emitIlParserFactory.GetParser(); - } + [Benchmark] + public Func EmitIl() + { + return _emitIlParserFactory.GetParser(); + } - [Benchmark] - public Func ExpressionTree() - { - return _expressionTreeParserFactory.GetParser(); - } + [Benchmark] + public Func ExpressionTree() + { + return _expressionTreeParserFactory.GetParser(); + } - [Benchmark] - public Func Sigil() - { - return _sigilParserFactory.GetParser(); - } + [Benchmark] + public Func Sigil() + { + return _sigilParserFactory.GetParser(); + } - [Benchmark] - public Func Roslyn() - { - return RoslynParserInitializer.CreateFactory().GetParser(); - } + [Benchmark] + public Func Roslyn() + { + return RoslynParserInitializer.CreateFactory().GetParser(); } -} +} \ No newline at end of file diff --git a/benchmark/ParserInvocation_Benchmark.cs b/benchmark/ParserInvocation_Benchmark.cs index 202456b..a410d25 100644 --- a/benchmark/ParserInvocation_Benchmark.cs +++ b/benchmark/ParserInvocation_Benchmark.cs @@ -1,86 +1,85 @@ using System; using BenchmarkDotNet.Attributes; -namespace Parsers.Benchmarks +namespace Parsers.Benchmarks; + +// ReSharper disable once InconsistentNaming +[MemoryDiagnoser] +public class ParserInvocation_Benchmark { - // ReSharper disable once InconsistentNaming - [MemoryDiagnoser] - public class ParserInvocation_Benchmark - { - private Func _emitIlParser; - private Func _expressionTreeParser; - private Func _reflectionParser; - private Func _sigilParser; - private Func _roslynParser; - private Func _sourceGeneratorParser; + private Func _emitIlParser; + private Func _expressionTreeParser; + private Func _reflectionParser; + private Func _sigilParser; + private Func _roslynParser; + private Func _sourceGeneratorParser; - private static readonly string[] Input = { "one", "1994-11-05T13:15:30", "22" }; + private static readonly string[] Input = { "one", "1994-11-05T13:15:30", "22" }; - [GlobalSetup] - public void GlobalSetup() - { - _emitIlParser = new EmitIlParserFactory().GetParser(); - _expressionTreeParser = new ExpressionTreeParserFactory().GetParser(); - _reflectionParser = new ReflectionParserFactory().GetParser(); - _sigilParser = new SigilParserFactory().GetParser(); - _roslynParser = RoslynParserInitializer.CreateFactory().GetParser(); - // ReSharper disable once PossibleNullReferenceException - _sourceGeneratorParser = ((IParserFactory)Activator.CreateInstance(Type.GetType("BySourceGenerator.Parser"))).GetParser(); - } + [GlobalSetup] + public void GlobalSetup() + { + _emitIlParser = new EmitIlParserFactory().GetParser(); + _expressionTreeParser = new ExpressionTreeParserFactory().GetParser(); + _reflectionParser = new ReflectionParserFactory().GetParser(); + _sigilParser = new SigilParserFactory().GetParser(); + _roslynParser = RoslynParserInitializer.CreateFactory().GetParser(); + // ReSharper disable once PossibleNullReferenceException + _sourceGeneratorParser = ((IParserFactory)Activator.CreateInstance(Type.GetType("BySourceGenerator.Parser"))).GetParser(); + } - [Benchmark] - public Data EmitIl() - { - return _emitIlParser.Invoke(Input); - } + [Benchmark] + public Data EmitIl() + { + return _emitIlParser.Invoke(Input); + } - [Benchmark] - public Data ExpressionTree() - { - return _expressionTreeParser.Invoke(Input); - } + [Benchmark] + public Data ExpressionTree() + { + return _expressionTreeParser.Invoke(Input); + } - [Benchmark] - public Data Reflection() - { - return _reflectionParser.Invoke(Input); - } + [Benchmark] + public Data Reflection() + { + return _reflectionParser.Invoke(Input); + } - [Benchmark] - public Data Sigil() - { - return _sigilParser.Invoke(Input); - } + [Benchmark] + public Data Sigil() + { + return _sigilParser.Invoke(Input); + } - [Benchmark] - public Data Roslyn() + [Benchmark] + public Data Roslyn() + { + return _roslynParser.Invoke(Input); + } + + [Benchmark] + public Data SourceGenerator() + { + return _sourceGeneratorParser.Invoke(Input); + } + + [Benchmark(Baseline = true)] + public Data ManuallyWritten() + { + var data = new Data(); + if (0 < Input.Length) { - return _roslynParser.Invoke(Input); + data.Name = Input[0]; } - - [Benchmark] - public Data SourceGenerator() + if (1 < Input.Length && DateTime.TryParse(Input[1], out var bd)) { - return _sourceGeneratorParser.Invoke(Input); + data.Birthday = bd; } - - [Benchmark(Baseline = true)] - public Data ManuallyWritten() + if (2 < Input.Length && int.TryParse(Input[2], out var n)) { - var data = new Data(); - if (0 < Input.Length) - { - data.Name = Input[0]; - } - if (1 < Input.Length && DateTime.TryParse(Input[1], out var bd)) - { - data.Birthday = bd; - } - if (2 < Input.Length && int.TryParse(Input[2], out var n)) - { - data.Number = n; - } - return data; + data.Number = n; } + return data; } -} +} \ No newline at end of file diff --git a/benchmark/Program.cs b/benchmark/Program.cs index 9d5657f..c0131e4 100644 --- a/benchmark/Program.cs +++ b/benchmark/Program.cs @@ -1,30 +1,17 @@ using BenchmarkDotNet.Running; +using Parsers.Benchmarks; -namespace Parsers.Benchmarks +var mode = args.Length == 1 ? args[0] : "all"; +switch (mode) { - class Program - { - private const string AllMode = "all"; - private const string GetParserMode = "gp"; - private const string ParserInvocationMode = "pi"; - - static void Main(string[] args) - { - var mode = args.Length == 1 ? args[0] : AllMode; - - switch (mode) - { - case GetParserMode: - BenchmarkRunner.Run(); - break; - case ParserInvocationMode: - BenchmarkRunner.Run(); - break; - default: - BenchmarkRunner.Run(); - BenchmarkRunner.Run(); - break; - } - } - } -} + case "gp": + BenchmarkRunner.Run(); + break; + case "pi": + BenchmarkRunner.Run(); + break; + default: + BenchmarkRunner.Run(); + BenchmarkRunner.Run(); + break; +} \ No newline at end of file diff --git a/src/Parsers.SourceGenerator/ParserSourceGenerator.cs b/src/Parsers.SourceGenerator/ParserSourceGenerator.cs index 5339e13..f7e53bd 100644 --- a/src/Parsers.SourceGenerator/ParserSourceGenerator.cs +++ b/src/Parsers.SourceGenerator/ParserSourceGenerator.cs @@ -6,143 +6,160 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; -namespace Parsers.SourceGenerator +namespace Parsers.SourceGenerator; + +/// +/// http://dontcodetired.com/blog/post/C-Source-Generators-Less-Boilerplate-Code-More-Productivity +/// https://github.com/amis92/csharp-source-generators +/// +/// https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.cookbook.md +/// https://blog.jetbrains.com/dotnet/2023/07/13/debug-source-generators-in-jetbrains-rider/ +/// https://andrewlock.net/exploring-dotnet-6-part-9-source-generator-updates-incremental-generators/#creating-a-source-generator-with-the-loggermessage-source-generator- +/// +[Generator] +public class ParserSourceGenerator : IIncrementalGenerator { - /// - /// http://dontcodetired.com/blog/post/C-Source-Generators-Less-Boilerplate-Code-More-Productivity - /// https://github.com/amis92/csharp-source-generators - /// - /// https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.cookbook.md - /// https://blog.jetbrains.com/dotnet/2023/07/13/debug-source-generators-in-jetbrains-rider/ - /// https://andrewlock.net/exploring-dotnet-6-part-9-source-generator-updates-incremental-generators/#creating-a-source-generator-with-the-loggermessage-source-generator- - /// - [Generator] - public class ParserSourceGenerator : IIncrementalGenerator + public void Initialize(IncrementalGeneratorInitializationContext context) { - public void Initialize(IncrementalGeneratorInitializationContext context) - { - var typesToParse = context.SyntaxProvider - .CreateSyntaxProvider( - predicate: static (syntaxNode, _) => syntaxNode is ClassDeclarationSyntax { AttributeLists.Count: > 0 }, - transform: (ctx, _) => - { - var clasDeclaration = (ClassDeclarationSyntax)ctx.Node; - - var parserOutputTypeSymbol = ctx.SemanticModel.Compilation - .GetTypeByMetadataName("Parsers.ParserOutputAttribute"); - - var symbol = ctx.SemanticModel.GetDeclaredSymbol(clasDeclaration) as INamedTypeSymbol; - if (symbol == null) return null; - - return symbol.GetAttributes().Any(a => - SymbolEqualityComparer.Default.Equals(a.AttributeClass, parserOutputTypeSymbol)) - ? symbol - : null; - }) - .Where(static m => m is not null) - .Collect(); + var classDeclarations = context.SyntaxProvider + .CreateSyntaxProvider( + predicate: static (syntaxNode, _) => syntaxNode is ClassDeclarationSyntax + { + AttributeLists.Count: > 0 + }, + transform: (ctx, _) => + { + var classDeclaration = (ClassDeclarationSyntax)ctx.Node; + return ctx.SemanticModel.GetDeclaredSymbol(classDeclaration) as INamedTypeSymbol; + }) + .Where(static symbol => symbol is not null); - context.RegisterSourceOutput( - context.CompilationProvider.Combine(typesToParse), - (k, f ) => + var parserOutputAttrSymbol = context.CompilationProvider + .Select((compilation, _) => compilation.GetTypeByMetadataName("Parsers.ParserOutputAttribute")); + + var classSymbolsArray = classDeclarations + .Combine(parserOutputAttrSymbol) + .Select((l, _) => + { + var (symbol, parserAttr) = l; + if (parserAttr is null) return null; + + return symbol.GetAttributes().Any(a => + SymbolEqualityComparer.Default.Equals(a.AttributeClass, parserAttr)) + ? symbol + : null; + }) + .Where(static symbol => symbol is not null) + .Collect(); + + context.RegisterSourceOutput( + context.CompilationProvider.Combine(classSymbolsArray), + (sourceProductionContext, f ) => { var attributeIndexTypeSymbol = f.Left.GetTypeByMetadataName("Parsers.ArrayIndexAttribute"); - GenerateSerializer(k, f.Right, attributeIndexTypeSymbol); + GenerateSerializer(sourceProductionContext, f.Right, attributeIndexTypeSymbol); }); - } + } - private static void GenerateSerializer(SourceProductionContext context, ImmutableArray classSymbol, INamedTypeSymbol attributeIndexTypeSymbol) - { - var typeNames = new List<(string TargetTypeName, string TargetTypeFullName, string TargetTypeParserName)>(); - var builder = new StringBuilder(); - builder.AppendLine(@" + private static void GenerateSerializer(SourceProductionContext context, ImmutableArray classSymbol, INamedTypeSymbol attributeIndexTypeSymbol) + { + var typeNames = new List<(string TargetTypeName, string TargetTypeFullName, string TargetTypeParserName)>(); + var builder = new StringBuilder(); + builder.AppendLine(@" using System; using Parsers; -namespace BySourceGenerator -{ + +namespace BySourceGenerator; + public class Parser : IParserFactory {"); - foreach (var typeSymbol in classSymbol.OfType()) + foreach (var typeSymbol in classSymbol) + { + var targetTypeName = typeSymbol.Name; + var targetTypeFullName = GetFullName(typeSymbol); + var targetTypeParserName = targetTypeName + "Parser"; + typeNames.Add((targetTypeName, targetTypeFullName, targetTypeParserName)); + builder.AppendLine($@" + private static T {targetTypeParserName}(string[] input)"); + + builder.Append($@" + {{ + var {targetTypeName}Instance = new {targetTypeFullName}();"); + + var props = typeSymbol.GetMembers().OfType(); + foreach (var prop in props) { - var targetTypeName = typeSymbol.Name; - var targetTypeFullName = GetFullName(typeSymbol); - var targetTypeParserName = targetTypeName + "Parser"; - typeNames.Add((targetTypeName, targetTypeFullName, targetTypeParserName)); - builder.AppendLine($"private static T {targetTypeParserName}(string[] input)"); - - builder.Append($@" -{{ - var {targetTypeName}Instance = new {targetTypeFullName}();"); - - var props = typeSymbol.GetMembers().OfType(); - foreach (var prop in props) + var attr = prop.GetAttributes().FirstOrDefault(x => SymbolEqualityComparer.Default.Equals(x.AttributeClass, attributeIndexTypeSymbol)); + if (attr == null || attr.ConstructorArguments[0].Value is not int) continue; + + int order = (int) attr.ConstructorArguments[0].Value; + if (order < 0) continue; + + if (GetFullName(prop.Type) == "System.String") { - var attr = prop.GetAttributes().FirstOrDefault(x => x.AttributeClass == attributeIndexTypeSymbol); - if (attr == null || attr.ConstructorArguments[0].Value is not int) continue; - - int order = (int) attr.ConstructorArguments[0].Value; - if (order < 0) continue; - - if (GetFullName(prop.Type) == "System.String") - { - builder.Append($@" -if({order} < input.Length) -{{ - {targetTypeName}Instance.{prop.Name} = input[{order}]; -}} -"); - } - - if (GetFullName(prop.Type) == "System.Int32") - { - builder.Append($@" -if({order} < input.Length && int.TryParse(input[{order}], out var parsed{prop.Name})) -{{ - {targetTypeName}Instance.{prop.Name} = parsed{prop.Name}; -}} -"); - } - - if (GetFullName(prop.Type) == "System.DateTime") - { - builder.Append($@" -if({order} < input.Length && DateTime.TryParse(input[{order}], out var parsed{prop.Name})) -{{ - {targetTypeName}Instance.{prop.Name} = parsed{prop.Name}; -}} -"); - } + builder.Append($@" + if({order} < input.Length) + {{ + {targetTypeName}Instance.{prop.Name} = input[{order}]; + }} + "); } - builder.Append($@" - object obj = {targetTypeName}Instance; - return (T)obj; -}}"); - } + if (GetFullName(prop.Type) == "System.Int32") + { + builder.Append($@" + if({order} < input.Length && int.TryParse(input[{order}], out var parsed{prop.Name})) + {{ + {targetTypeName}Instance.{prop.Name} = parsed{prop.Name}; + }} + "); + } - builder.AppendLine("public Func GetParser() where T : new() {"); - foreach (var typeName in typeNames) - { - builder.Append($@" -if (typeof(T) == typeof({typeName.TargetTypeFullName})) -{{ - return {typeName.TargetTypeParserName}; -}} -"); + if (GetFullName(prop.Type) == "System.DateTime") + { + builder.Append($@" + if({order} < input.Length && DateTime.TryParse(input[{order}], out var parsed{prop.Name})) + {{ + {targetTypeName}Instance.{prop.Name} = parsed{prop.Name}; + }} + "); + } } - builder.AppendLine("throw new NotSupportedException();}"); - - builder.AppendLine("}}"); + builder.Append($@" + object obj = {targetTypeName}Instance; + return (T)obj; + }}"); + } - var src = builder.ToString(); - context.AddSource( - "ParserGeneratedBySourceGenerator.cs", - SourceText.From(src, Encoding.UTF8) - ); + builder.AppendLine(@" + public Func GetParser() where T : new() + {"); + foreach (var typeName in typeNames) + { + builder.Append($@" + if (typeof(T) == typeof({typeName.TargetTypeFullName})) + {{ + return {typeName.TargetTypeParserName}; + }} + "); } - private static string GetFullName(ITypeSymbol typeSymbol) => - $"{typeSymbol.ContainingNamespace}.{typeSymbol.Name}"; + builder.AppendLine(@" + + throw new NotSupportedException(); + }"); + + builder.AppendLine(@" +}"); + + var src = builder.ToString(); + context.AddSource( + "ParserGeneratedBySourceGenerator.cs", + SourceText.From(src, Encoding.UTF8) + ); } + + private static string GetFullName(ITypeSymbol typeSymbol) => + $"{typeSymbol.ContainingNamespace}.{typeSymbol.Name}"; } \ No newline at end of file diff --git a/src/Parsers.SourceGenerator/Properties/launchSettings.json b/src/Parsers.SourceGenerator/Properties/launchSettings.json index 8353097..0b975fe 100644 --- a/src/Parsers.SourceGenerator/Properties/launchSettings.json +++ b/src/Parsers.SourceGenerator/Properties/launchSettings.json @@ -1,7 +1,7 @@ { "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { - "Generators": { + "DebugInTests": { "commandName": "DebugRoslynComponent", "targetProject": "../../test/Parsers.Tests.csproj" } diff --git a/src/Parsers/Attributes.cs b/src/Parsers/Attributes.cs index b392d66..cac44c5 100644 --- a/src/Parsers/Attributes.cs +++ b/src/Parsers/Attributes.cs @@ -1,19 +1,13 @@ using System; -namespace Parsers -{ - [AttributeUsage(AttributeTargets.Class)] - public sealed class ParserOutputAttribute : Attribute - { } +namespace Parsers; - [AttributeUsage(AttributeTargets.Property)] - public sealed class ArrayIndexAttribute : Attribute - { - public ArrayIndexAttribute(int order) - { - Order = order; - } +[AttributeUsage(AttributeTargets.Class)] +public sealed class ParserOutputAttribute : Attribute +{ } - public int Order { get; } - } +[AttributeUsage(AttributeTargets.Property)] +public sealed class ArrayIndexAttribute(int order) : Attribute +{ + public int Order { get; } = order; } \ No newline at end of file diff --git a/src/Parsers/CachedParserFactory.cs b/src/Parsers/CachedParserFactory.cs index 3518364..b6cf606 100644 --- a/src/Parsers/CachedParserFactory.cs +++ b/src/Parsers/CachedParserFactory.cs @@ -2,23 +2,15 @@ using System.Collections.Concurrent; using System.Threading; -namespace Parsers -{ - public class CachedParserFactory : IParserFactory - { - private readonly IParserFactory _realParserFactory; - private readonly ConcurrentDictionary> _cache; +namespace Parsers; - public CachedParserFactory(IParserFactory realParserFactory) - { - _realParserFactory = realParserFactory; - _cache = new ConcurrentDictionary>(); - } +public class CachedParserFactory(IParserFactory realParserFactory) : IParserFactory +{ + private readonly ConcurrentDictionary> _cache = new(); - public Func GetParser() where T : new() - { - return (Func)(_cache.GetOrAdd($"aip_{_realParserFactory.GetType().FullName}_{typeof(T).FullName}", - new Lazy(() => _realParserFactory.GetParser(), LazyThreadSafetyMode.ExecutionAndPublication)).Value); - } + public Func GetParser() where T : new() + { + return (Func)(_cache.GetOrAdd($"aip_{realParserFactory.GetType().FullName}_{typeof(T).FullName}", + new Lazy(realParserFactory.GetParser, LazyThreadSafetyMode.ExecutionAndPublication)).Value); } -} +} \ No newline at end of file diff --git a/src/Parsers/EmitIlParserFactory.cs b/src/Parsers/EmitIlParserFactory.cs index aec216e..343572e 100644 --- a/src/Parsers/EmitIlParserFactory.cs +++ b/src/Parsers/EmitIlParserFactory.cs @@ -3,79 +3,78 @@ using System.Reflection; using System.Reflection.Emit; -namespace Parsers +namespace Parsers; + +public class EmitIlParserFactory : IParserFactory { - public class EmitIlParserFactory : IParserFactory + public Func GetParser() where T : new() { - public Func GetParser() where T : new() - { - var props = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public); - - var dm = new DynamicMethod($"from_{typeof(string[]).FullName}_to_{typeof(T).FullName}", - typeof(T), new [] { typeof(string[]) }, typeof(EmitIlParserFactory).Module); - var il = dm.GetILGenerator(); - - var instance = il.DeclareLocal(typeof(T)); - il.Emit(OpCodes.Newobj, typeof(T).GetConstructors()[0]); - il.Emit(OpCodes.Stloc, instance); - - foreach (var prop in props) - { - var attrs = prop.GetCustomAttributes(typeof(ArrayIndexAttribute)).ToArray(); - if (attrs.Length == 0) continue; + var props = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public); - int order = ((ArrayIndexAttribute)attrs[0]).Order; - if (order < 0) continue; + var dm = new DynamicMethod($"from_{typeof(string[]).FullName}_to_{typeof(T).FullName}", + typeof(T), new [] { typeof(string[]) }, typeof(EmitIlParserFactory).Module); + var il = dm.GetILGenerator(); - var label = il.DefineLabel(); + var instance = il.DeclareLocal(typeof(T)); + il.Emit(OpCodes.Newobj, typeof(T).GetConstructors()[0]); + il.Emit(OpCodes.Stloc, instance); - if (prop.PropertyType == typeof(string)) - { - il.Emit(OpCodes.Ldc_I4, order); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldlen); - il.Emit(OpCodes.Bge_S, label); - - il.Emit(OpCodes.Ldloc, instance); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldc_I4, order); - il.Emit(OpCodes.Ldelem_Ref); - il.Emit(OpCodes.Callvirt, prop.GetSetMethod()); + foreach (var prop in props) + { + var attrs = prop.GetCustomAttributes(typeof(ArrayIndexAttribute)).ToArray(); + if (attrs.Length == 0) continue; - il.MarkLabel(label); - continue; - } + int order = ((ArrayIndexAttribute)attrs[0]).Order; + if (order < 0) continue; - if (!TypeParsers.Parsers.TryGetValue(prop.PropertyType, out var parser)) - { - continue; - } + var label = il.DefineLabel(); + if (prop.PropertyType == typeof(string)) + { il.Emit(OpCodes.Ldc_I4, order); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldlen); il.Emit(OpCodes.Bge_S, label); - var parseResult = il.DeclareLocal(prop.PropertyType); - + il.Emit(OpCodes.Ldloc, instance); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldc_I4, order); il.Emit(OpCodes.Ldelem_Ref); - il.Emit(OpCodes.Ldloca, parseResult); - il.EmitCall(OpCodes.Call, parser, null); - il.Emit(OpCodes.Brfalse_S, label); - - il.Emit(OpCodes.Ldloc, instance); - il.Emit(OpCodes.Ldloc, parseResult); il.Emit(OpCodes.Callvirt, prop.GetSetMethod()); il.MarkLabel(label); + continue; + } + + if (!TypeParsers.Parsers.TryGetValue(prop.PropertyType, out var parser)) + { + continue; } + il.Emit(OpCodes.Ldc_I4, order); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldlen); + il.Emit(OpCodes.Bge_S, label); + + var parseResult = il.DeclareLocal(prop.PropertyType); + + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldc_I4, order); + il.Emit(OpCodes.Ldelem_Ref); + il.Emit(OpCodes.Ldloca, parseResult); + il.EmitCall(OpCodes.Call, parser, null); + il.Emit(OpCodes.Brfalse_S, label); + il.Emit(OpCodes.Ldloc, instance); - il.Emit(OpCodes.Ret); + il.Emit(OpCodes.Ldloc, parseResult); + il.Emit(OpCodes.Callvirt, prop.GetSetMethod()); - return (Func)dm.CreateDelegate(typeof(Func)); + il.MarkLabel(label); } + + il.Emit(OpCodes.Ldloc, instance); + il.Emit(OpCodes.Ret); + + return (Func)dm.CreateDelegate(typeof(Func)); } -} +} \ No newline at end of file diff --git a/src/Parsers/ExpressionTreeParserFactory.cs b/src/Parsers/ExpressionTreeParserFactory.cs index eb70d98..b7c711c 100644 --- a/src/Parsers/ExpressionTreeParserFactory.cs +++ b/src/Parsers/ExpressionTreeParserFactory.cs @@ -4,66 +4,65 @@ using System.Linq.Expressions; using System.Reflection; -namespace Parsers +namespace Parsers; + +public class ExpressionTreeParserFactory : IParserFactory { - public class ExpressionTreeParserFactory : IParserFactory + public Func GetParser() where T : new() { - public Func GetParser() where T : new() - { - var props = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public); - - ParameterExpression inputArray = Expression.Parameter(typeof(string[]), "inputArray"); - ParameterExpression instance = Expression.Variable(typeof(T), "instance"); - - var block = new List - { - Expression.Assign(instance, Expression.New(typeof(T).GetConstructors()[0])) - }; - var variables = new List {instance}; - - foreach (var prop in props) - { - var attrs = prop.GetCustomAttributes(typeof(ArrayIndexAttribute)).ToArray(); - if (attrs.Length == 0) continue; + var props = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public); - int order = ((ArrayIndexAttribute)attrs[0]).Order; - if (order < 0) continue; + ParameterExpression inputArray = Expression.Parameter(typeof(string[]), "inputArray"); + ParameterExpression instance = Expression.Variable(typeof(T), "instance"); - var orderConst = Expression.Constant(order); - var orderCheck = Expression.LessThan(orderConst, Expression.ArrayLength(inputArray)); + var block = new List + { + Expression.Assign(instance, Expression.New(typeof(T).GetConstructors()[0])) + }; + var variables = new List {instance}; - if (prop.PropertyType == typeof(string)) - { - var stringPropertySet = Expression.Assign( - Expression.Property(instance, prop), - Expression.ArrayIndex(inputArray, orderConst)); + foreach (var prop in props) + { + var attrs = prop.GetCustomAttributes(typeof(ArrayIndexAttribute)).ToArray(); + if (attrs.Length == 0) continue; - block.Add(Expression.IfThen(orderCheck, stringPropertySet)); - continue; - } + int order = ((ArrayIndexAttribute)attrs[0]).Order; + if (order < 0) continue; - if (!TypeParsers.Parsers.TryGetValue(prop.PropertyType, out var parser)) - { - continue; - } + var orderConst = Expression.Constant(order); + var orderCheck = Expression.LessThan(orderConst, Expression.ArrayLength(inputArray)); - var parseResult = Expression.Variable(prop.PropertyType, "parseResult"); - var parserCall = Expression.Call(parser, Expression.ArrayIndex(inputArray, orderConst), parseResult); - var propertySet = Expression.Assign( + if (prop.PropertyType == typeof(string)) + { + var stringPropertySet = Expression.Assign( Expression.Property(instance, prop), - parseResult); + Expression.ArrayIndex(inputArray, orderConst)); - var ifSet = Expression.IfThen(parserCall, propertySet); + block.Add(Expression.IfThen(orderCheck, stringPropertySet)); + continue; + } - block.Add(Expression.IfThen(orderCheck, ifSet)); - variables.Add(parseResult); + if (!TypeParsers.Parsers.TryGetValue(prop.PropertyType, out var parser)) + { + continue; } - block.Add(instance); + var parseResult = Expression.Variable(prop.PropertyType, "parseResult"); + var parserCall = Expression.Call(parser, Expression.ArrayIndex(inputArray, orderConst), parseResult); + var propertySet = Expression.Assign( + Expression.Property(instance, prop), + parseResult); - return Expression.Lambda>( - Expression.Block(variables.ToArray(), Expression.Block(block)), - inputArray).Compile(); + var ifSet = Expression.IfThen(parserCall, propertySet); + + block.Add(Expression.IfThen(orderCheck, ifSet)); + variables.Add(parseResult); } + + block.Add(instance); + + return Expression.Lambda>( + Expression.Block(variables.ToArray(), Expression.Block(block)), + inputArray).Compile(); } -} +} \ No newline at end of file diff --git a/src/Parsers/IParserFactory.cs b/src/Parsers/IParserFactory.cs index 1eb0a93..66aa9ef 100644 --- a/src/Parsers/IParserFactory.cs +++ b/src/Parsers/IParserFactory.cs @@ -1,9 +1,8 @@ using System; -namespace Parsers +namespace Parsers; + +public interface IParserFactory { - public interface IParserFactory - { - Func GetParser() where T : new(); - } -} + Func GetParser() where T : new(); +} \ No newline at end of file diff --git a/src/Parsers/ReflectionParserFactory.cs b/src/Parsers/ReflectionParserFactory.cs index fbf061b..ca64393 100644 --- a/src/Parsers/ReflectionParserFactory.cs +++ b/src/Parsers/ReflectionParserFactory.cs @@ -2,52 +2,51 @@ using System.Linq; using System.Reflection; -namespace Parsers +namespace Parsers; + +public class ReflectionParserFactory : IParserFactory { - public class ReflectionParserFactory : IParserFactory + public Func GetParser() where T : new() { - public Func GetParser() where T : new() - { - return ArrayIndexParse; - } + return ArrayIndexParse; + } - private static T ArrayIndexParse(string[] data) where T : new() + private static T ArrayIndexParse(string[] data) where T : new() + { + var instance = new T(); + var props = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public); + for (int i = 0; i < props.Length; i++) { - var instance = new T(); - var props = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public); - for (int i = 0; i < props.Length; i++) - { - var attrs = props[i].GetCustomAttributes(typeof(ArrayIndexAttribute)).ToArray(); - if (attrs.Length == 0) continue; + var attrs = props[i].GetCustomAttributes(typeof(ArrayIndexAttribute)).ToArray(); + if (attrs.Length == 0) continue; - int order = ((ArrayIndexAttribute)attrs[0]).Order; - if (order < 0 || order >= data.Length) continue; + int order = ((ArrayIndexAttribute)attrs[0]).Order; + if (order < 0 || order >= data.Length) continue; - if (props[i].PropertyType == typeof(string)) - { - props[i].SetValue(instance, data[order]); - continue; - } + if (props[i].PropertyType == typeof(string)) + { + props[i].SetValue(instance, data[order]); + continue; + } - if (props[i].PropertyType == typeof(int)) + if (props[i].PropertyType == typeof(int)) + { + if (int.TryParse(data[order], out var intResult)) { - if (int.TryParse(data[order], out var intResult)) - { - props[i].SetValue(instance, intResult); - } - - continue; + props[i].SetValue(instance, intResult); } - if (props[i].PropertyType == typeof(DateTime)) + continue; + } + + if (props[i].PropertyType == typeof(DateTime)) + { + if (DateTime.TryParse(data[order], out var dtResult)) { - if (DateTime.TryParse(data[order], out var dtResult)) - { - props[i].SetValue(instance, dtResult); - } + props[i].SetValue(instance, dtResult); } } - return instance; } + return instance; } -} +} \ No newline at end of file diff --git a/src/Parsers/RoslynParserInitializer.cs b/src/Parsers/RoslynParserInitializer.cs index 05d1e48..b126f8f 100644 --- a/src/Parsers/RoslynParserInitializer.cs +++ b/src/Parsers/RoslynParserInitializer.cs @@ -9,142 +9,141 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; -namespace Parsers +namespace Parsers; + +/// +/// https://www.tugberkugurlu.com/archive/compiling-c-sharp-code-into-memory-and-executing-it-with-roslyn +/// https://gunnarpeipman.com/using-roslyn-to-build-object-to-object-mapper/amp/ +/// https://www.youtube.com/watch?v=052xutD86uI +/// +public static class RoslynParserInitializer { - /// - /// https://www.tugberkugurlu.com/archive/compiling-c-sharp-code-into-memory-and-executing-it-with-roslyn - /// https://gunnarpeipman.com/using-roslyn-to-build-object-to-object-mapper/amp/ - /// https://www.youtube.com/watch?v=052xutD86uI - /// - public static class RoslynParserInitializer + public static IParserFactory CreateFactory() { - public static IParserFactory CreateFactory() - { - var targetTypes = - (from a in AppDomain.CurrentDomain.GetAssemblies() - from t in a.GetTypes() - let attributes = t.GetCustomAttributes(typeof(ParserOutputAttribute), true) - where attributes != null && attributes.Length > 0 - select t).ToArray(); - - var typeNames = new List<(string TargetTypeName, string TargetTypeFullName, string TargetTypeParserName)>(); - var builder = new StringBuilder(); - builder.AppendLine(@" + var targetTypes = + (from a in AppDomain.CurrentDomain.GetAssemblies() + from t in a.GetTypes() + let attributes = t.GetCustomAttributes(typeof(ParserOutputAttribute), true) + where attributes != null && attributes.Length > 0 + select t).ToArray(); + + var typeNames = new List<(string TargetTypeName, string TargetTypeFullName, string TargetTypeParserName)>(); + var builder = new StringBuilder(); + builder.AppendLine(@" using System; using Parsers; public class RoslynGeneratedParserFactory : IParserFactory {"); - foreach (var targetType in targetTypes) - { - var targetTypeName = targetType.Name; - var targetTypeFullName = targetType.FullName; - var targetTypeParserName = targetTypeName + "Parser"; - typeNames.Add((targetTypeName, targetTypeFullName, targetTypeParserName)); - builder.AppendLine($"private static T {targetTypeParserName}(string[] input)"); + foreach (var targetType in targetTypes) + { + var targetTypeName = targetType.Name; + var targetTypeFullName = targetType.FullName; + var targetTypeParserName = targetTypeName + "Parser"; + typeNames.Add((targetTypeName, targetTypeFullName, targetTypeParserName)); + builder.AppendLine($"private static T {targetTypeParserName}(string[] input)"); - builder.Append($@" + builder.Append($@" {{ var {targetTypeName}Instance = new {targetTypeFullName}();"); - var props = targetType.GetProperties(BindingFlags.Instance | BindingFlags.Public); - foreach (var prop in props) - { - var attrs = prop.GetCustomAttributes(typeof(ArrayIndexAttribute)).ToArray(); - if (attrs.Length == 0) continue; + var props = targetType.GetProperties(BindingFlags.Instance | BindingFlags.Public); + foreach (var prop in props) + { + var attrs = prop.GetCustomAttributes(typeof(ArrayIndexAttribute)).ToArray(); + if (attrs.Length == 0) continue; - int order = ((ArrayIndexAttribute)attrs[0]).Order; - if (order < 0) continue; + int order = ((ArrayIndexAttribute)attrs[0]).Order; + if (order < 0) continue; - if (prop.PropertyType == typeof(string)) - { - builder.Append($@" + if (prop.PropertyType == typeof(string)) + { + builder.Append($@" if({order} < input.Length) {{ {targetTypeName}Instance.{prop.Name} = input[{order}]; }} "); - } + } - if (prop.PropertyType == typeof(int)) - { - builder.Append($@" + if (prop.PropertyType == typeof(int)) + { + builder.Append($@" if({order} < input.Length && int.TryParse(input[{order}], out var parsed{prop.Name})) {{ {targetTypeName}Instance.{prop.Name} = parsed{prop.Name}; }} "); - } + } - if (prop.PropertyType == typeof(DateTime)) - { - builder.Append($@" + if (prop.PropertyType == typeof(DateTime)) + { + builder.Append($@" if({order} < input.Length && DateTime.TryParse(input[{order}], out var parsed{prop.Name})) {{ {targetTypeName}Instance.{prop.Name} = parsed{prop.Name}; }} "); - } } + } - builder.Append($@" + builder.Append($@" object obj = {targetTypeName}Instance; return (T)obj; }}"); - } + } - builder.AppendLine("public Func GetParser() where T : new() {"); - foreach (var typeName in typeNames) - { - builder.Append($@" + builder.AppendLine("public Func GetParser() where T : new() {"); + foreach (var typeName in typeNames) + { + builder.Append($@" if (typeof(T) == typeof({typeName.TargetTypeFullName})) {{ return {typeName.TargetTypeParserName}; }} "); - } - builder.AppendLine("throw new NotSupportedException();}"); + } + builder.AppendLine("throw new NotSupportedException();}"); - builder.AppendLine("}"); + builder.AppendLine("}"); - var syntaxTree = CSharpSyntaxTree.ParseText(builder.ToString()); + var syntaxTree = CSharpSyntaxTree.ParseText(builder.ToString()); - string assemblyName = Path.GetRandomFileName(); - var refPaths = new List { - typeof(Object).GetTypeInfo().Assembly.Location, - typeof(Enumerable).GetTypeInfo().Assembly.Location, - Path.Combine(Path.GetDirectoryName(typeof(GCSettings).GetTypeInfo().Assembly.Location), "System.Runtime.dll"), - typeof(RoslynParserInitializer).GetTypeInfo().Assembly.Location, - typeof(IParserFactory).GetTypeInfo().Assembly.Location, - Path.Combine(Path.GetDirectoryName(typeof(GCSettings).GetTypeInfo().Assembly.Location), "netstandard.dll"), - }; - refPaths.AddRange(targetTypes.Select(x => x.Assembly.Location)); + string assemblyName = Path.GetRandomFileName(); + var refPaths = new List { + typeof(Object).GetTypeInfo().Assembly.Location, + typeof(Enumerable).GetTypeInfo().Assembly.Location, + Path.Combine(Path.GetDirectoryName(typeof(GCSettings).GetTypeInfo().Assembly.Location), "System.Runtime.dll"), + typeof(RoslynParserInitializer).GetTypeInfo().Assembly.Location, + typeof(IParserFactory).GetTypeInfo().Assembly.Location, + Path.Combine(Path.GetDirectoryName(typeof(GCSettings).GetTypeInfo().Assembly.Location), "netstandard.dll"), + }; + refPaths.AddRange(targetTypes.Select(x => x.Assembly.Location)); - var references = refPaths.Select(r => MetadataReference.CreateFromFile(r)).ToArray(); + var references = refPaths.Select(r => MetadataReference.CreateFromFile(r)).ToArray(); - var compilation = CSharpCompilation.Create( - assemblyName, - syntaxTrees: new[] { syntaxTree }, - references: references, - options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + var compilation = CSharpCompilation.Create( + assemblyName, + syntaxTrees: new[] { syntaxTree }, + references: references, + options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); - using (var ms = new MemoryStream()) + using (var ms = new MemoryStream()) + { + var result = compilation.Emit(ms); + if (!result.Success) { - var result = compilation.Emit(ms); - if (!result.Success) - { - throw new Exception(string.Join(",", result.Diagnostics.Where(diagnostic => - diagnostic.IsWarningAsError || - diagnostic.Severity == DiagnosticSeverity.Error).Select(x => x.GetMessage()))); - } - ms.Seek(0, SeekOrigin.Begin); + throw new Exception(string.Join(",", result.Diagnostics.Where(diagnostic => + diagnostic.IsWarningAsError || + diagnostic.Severity == DiagnosticSeverity.Error).Select(x => x.GetMessage()))); + } + ms.Seek(0, SeekOrigin.Begin); - var assembly = AssemblyLoadContext.Default.LoadFromStream(ms); + var assembly = AssemblyLoadContext.Default.LoadFromStream(ms); - var factoryType = assembly.GetType("RoslynGeneratedParserFactory"); - if (factoryType == null) throw new NullReferenceException("Roslyn generated parser type not found"); - return (IParserFactory)Activator.CreateInstance(factoryType); - } + var factoryType = assembly.GetType("RoslynGeneratedParserFactory"); + if (factoryType == null) throw new NullReferenceException("Roslyn generated parser type not found"); + return (IParserFactory)Activator.CreateInstance(factoryType); } } -} +} \ No newline at end of file diff --git a/src/Parsers/SigilParserFactory.cs b/src/Parsers/SigilParserFactory.cs index 21bc83b..0216f63 100644 --- a/src/Parsers/SigilParserFactory.cs +++ b/src/Parsers/SigilParserFactory.cs @@ -3,77 +3,76 @@ using System.Reflection; using Sigil; -namespace Parsers +namespace Parsers; + +public class SigilParserFactory : IParserFactory { - public class SigilParserFactory : IParserFactory + public Func GetParser() where T : new() { - public Func GetParser() where T : new() - { - var props = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public); - - var il = Emit>.NewDynamicMethod($"from_{typeof(string[]).FullName}_to_{typeof(T).FullName}"); - - var instance = il.DeclareLocal(); - il.NewObject(); - il.StoreLocal(instance); - - foreach (var prop in props) - { - var attrs = prop.GetCustomAttributes(typeof(ArrayIndexAttribute)).ToArray(); - if (attrs.Length == 0) continue; + var props = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public); - int order = ((ArrayIndexAttribute)attrs[0]).Order; - if (order < 0) continue; + var il = Emit>.NewDynamicMethod($"from_{typeof(string[]).FullName}_to_{typeof(T).FullName}"); - var label = il.DefineLabel(); + var instance = il.DeclareLocal(); + il.NewObject(); + il.StoreLocal(instance); - if (prop.PropertyType == typeof(string)) - { - il.LoadConstant(order); - il.LoadArgument(0); - il.LoadLength(); - il.BranchIfGreaterOrEqual(label); - - il.LoadLocal(instance); - il.LoadArgument(0); - il.LoadConstant(order); - il.LoadElement(); - il.CallVirtual(prop.GetSetMethod()); + foreach (var prop in props) + { + var attrs = prop.GetCustomAttributes(typeof(ArrayIndexAttribute)).ToArray(); + if (attrs.Length == 0) continue; - il.MarkLabel(label); - continue; - } + int order = ((ArrayIndexAttribute)attrs[0]).Order; + if (order < 0) continue; - if (!TypeParsers.Parsers.TryGetValue(prop.PropertyType, out var parser)) - { - continue; - } + var label = il.DefineLabel(); + if (prop.PropertyType == typeof(string)) + { il.LoadConstant(order); il.LoadArgument(0); il.LoadLength(); il.BranchIfGreaterOrEqual(label); - var parseResult = il.DeclareLocal(prop.PropertyType); - + il.LoadLocal(instance); il.LoadArgument(0); il.LoadConstant(order); il.LoadElement(); - il.LoadLocalAddress(parseResult); - il.Call(parser); - il.BranchIfFalse(label); - - il.LoadLocal(instance); - il.LoadLocal(parseResult); il.CallVirtual(prop.GetSetMethod()); il.MarkLabel(label); + continue; + } + + if (!TypeParsers.Parsers.TryGetValue(prop.PropertyType, out var parser)) + { + continue; } + il.LoadConstant(order); + il.LoadArgument(0); + il.LoadLength(); + il.BranchIfGreaterOrEqual(label); + + var parseResult = il.DeclareLocal(prop.PropertyType); + + il.LoadArgument(0); + il.LoadConstant(order); + il.LoadElement(); + il.LoadLocalAddress(parseResult); + il.Call(parser); + il.BranchIfFalse(label); + il.LoadLocal(instance); - il.Return(); + il.LoadLocal(parseResult); + il.CallVirtual(prop.GetSetMethod()); - return il.CreateDelegate(); + il.MarkLabel(label); } + + il.LoadLocal(instance); + il.Return(); + + return il.CreateDelegate(); } -} +} \ No newline at end of file diff --git a/src/Parsers/TypeParsers.cs b/src/Parsers/TypeParsers.cs index cc34fa3..6033427 100644 --- a/src/Parsers/TypeParsers.cs +++ b/src/Parsers/TypeParsers.cs @@ -2,14 +2,13 @@ using System.Collections.Generic; using System.Reflection; -namespace Parsers +namespace Parsers; + +internal static class TypeParsers { - internal static class TypeParsers + public static readonly Dictionary Parsers = new() { - public static readonly Dictionary Parsers = new Dictionary - { - { typeof(int), typeof(int).GetMethod("TryParse", new[] {typeof(string), typeof(int).MakeByRefType()}) }, - { typeof(DateTime), typeof(DateTime).GetMethod("TryParse", new[] {typeof(string), typeof(DateTime).MakeByRefType()}) } - }; - } -} + { typeof(int), typeof(int).GetMethod("TryParse", [typeof(string), typeof(int).MakeByRefType()]) }, + { typeof(DateTime), typeof(DateTime).GetMethod("TryParse", [typeof(string), typeof(DateTime).MakeByRefType()]) } + }; +} \ No newline at end of file diff --git a/test/Data.cs b/test/Data.cs index a60ca55..693fe7c 100644 --- a/test/Data.cs +++ b/test/Data.cs @@ -1,20 +1,19 @@ using System; -namespace Parsers.Tests +namespace Parsers.Tests; + +[ParserOutput] +public class Data { - [ParserOutput] - public class Data - { - [ArrayIndex(0)] - public string Name { get; set; } + [ArrayIndex(0)] + public string Name { get; set; } - [ArrayIndex(2)] - public int Number { get; set; } + [ArrayIndex(2)] + public int Number { get; set; } - [ArrayIndex(1)] - public DateTime Birthday { get; set; } + [ArrayIndex(1)] + public DateTime Birthday { get; set; } - [ArrayIndex(int.MaxValue)] - public string PropertyWithInvalidAttr { get; set; } - } -} + [ArrayIndex(int.MaxValue)] + public string PropertyWithInvalidAttr { get; set; } +} \ No newline at end of file diff --git a/test/ParserGeneratorTests.cs b/test/ParserGeneratorTests.cs index 18fed7a..0eaef98 100644 --- a/test/ParserGeneratorTests.cs +++ b/test/ParserGeneratorTests.cs @@ -3,49 +3,48 @@ using FluentAssertions; using Xunit; -namespace Parsers.Tests +namespace Parsers.Tests; + +public class ParserGeneratorTests { - public class ParserGeneratorTests + [Theory] + [MemberData(nameof(Factories))] + public void GetParserUsingArrayIndex_ValidInput_Parsed(IParserFactory parserFactory) + { + var p = parserFactory.GetParser(); + + var inst = p.Invoke(new[] { "one", "1994-11-05T13:15:30", "22" }); + + inst.Name.Should().Be("one"); + inst.Birthday.Should().Be(new DateTime(1994, 11, 5, 13, 15, 30)); + inst.Number.Should().Be(22); + inst.PropertyWithInvalidAttr.Should().Be(null); + } + + [Theory] + [MemberData(nameof(Factories))] + public void GetParserUsingArrayIndex_InvalidInput_DefaultValuesNoException(IParserFactory parserFactory) { - [Theory] - [MemberData(nameof(Factories))] - public void GetParserUsingArrayIndex_ValidInput_Parsed(IParserFactory parserFactory) - { - var p = parserFactory.GetParser(); - - var inst = p.Invoke(new[] { "one", "1994-11-05T13:15:30", "22" }); - - inst.Name.Should().Be("one"); - inst.Birthday.Should().Be(new DateTime(1994, 11, 5, 13, 15, 30)); - inst.Number.Should().Be(22); - inst.PropertyWithInvalidAttr.Should().Be(null); - } - - [Theory] - [MemberData(nameof(Factories))] - public void GetParserUsingArrayIndex_InvalidInput_DefaultValuesNoException(IParserFactory parserFactory) - { - var p = parserFactory.GetParser(); - - var inst = p.Invoke(new[] { "one", "2011ss-11-22", "2vv2" }); - - inst.Name.Should().Be("one"); - inst.Birthday.Should().Be(DateTime.MinValue); - inst.Number.Should().Be(default(int)); - inst.PropertyWithInvalidAttr.Should().Be(null); - } - - public static IEnumerable Factories => new List - { - new object[] { new ExpressionTreeParserFactory() }, - new object[] { new CachedParserFactory(new ExpressionTreeParserFactory()) }, - new object[] { new EmitIlParserFactory() }, - new object[] { new CachedParserFactory(new EmitIlParserFactory()) }, - new object[] { new SigilParserFactory() }, - new object[] { new CachedParserFactory(new SigilParserFactory()) }, - new object[] { RoslynParserInitializer.CreateFactory() }, - new object[] { new BySourceGenerator.Parser() }, - new object[] { new ReflectionParserFactory() }, - }; + var p = parserFactory.GetParser(); + + var inst = p.Invoke(new[] { "one", "2011ss-11-22", "2vv2" }); + + inst.Name.Should().Be("one"); + inst.Birthday.Should().Be(DateTime.MinValue); + inst.Number.Should().Be(default(int)); + inst.PropertyWithInvalidAttr.Should().Be(null); } -} + + public static IEnumerable Factories => new List + { + new object[] { new ExpressionTreeParserFactory() }, + new object[] { new CachedParserFactory(new ExpressionTreeParserFactory()) }, + new object[] { new EmitIlParserFactory() }, + new object[] { new CachedParserFactory(new EmitIlParserFactory()) }, + new object[] { new SigilParserFactory() }, + new object[] { new CachedParserFactory(new SigilParserFactory()) }, + new object[] { RoslynParserInitializer.CreateFactory() }, + new object[] { new BySourceGenerator.Parser() }, + new object[] { new ReflectionParserFactory() }, + }; +} \ No newline at end of file From 57b4e201bd4f3f5a72a864ef28b2b4b13270ec9d Mon Sep 17 00:00:00 2001 From: Maxim Tkachenko Date: Mon, 13 Oct 2025 18:10:04 +0200 Subject: [PATCH 6/7] updated benchmark results --- README.md | 40 ++++++++++++++++++++-------------------- benchmark/run.cmd | 2 -- benchmark/run.sh | 1 + 3 files changed, 21 insertions(+), 22 deletions(-) delete mode 100644 benchmark/run.cmd create mode 100755 benchmark/run.sh diff --git a/README.md b/README.md index 5959d90..402387c 100644 --- a/README.md +++ b/README.md @@ -4,30 +4,30 @@ Source code for [Dotnet code generation overview by example](https://mtkachenko. ``` ini -BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1237 (21H1/May2021Update) -Intel Core i7-8550U CPU 1.80GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores -.NET SDK=5.0.401 - [Host] : .NET 5.0.10 (5.0.1021.41214), X64 RyuJIT - DefaultJob : .NET 5.0.10 (5.0.1021.41214), X64 RyuJIT +BenchmarkDotNet v0.15.4, macOS 26.0.1 (25A362) [Darwin 25.0.0] +Apple M4, 1 CPU, 10 logical and 10 physical cores +.NET SDK 9.0.305 + [Host] : .NET 9.0.9 (9.0.9, 9.0.925.41916), Arm64 RyuJIT armv8.0-a + DefaultJob : .NET 9.0.9 (9.0.9, 9.0.925.41916), Arm64 RyuJIT armv8.0-a ``` ## Generation of parser -| Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated | -|--------------- |-------------:|-------------:|-------------:|----------:|-------:|-------:|----------:| -| EmitIl | 22.02 μs | 0.495 μs | 1.429 μs | 1.2817 | 0.6409 | 0.0305 | 5 KB | -| ExpressionTree | 683.68 μs | 13.609 μs | 31.268 μs | 2.9297 | 0.9766 | - | 14 KB | -| Sigil | 642.63 μs | 12.305 μs | 29.243 μs | 112.3047 | - | - | 460 KB | -| Roslyn | 71,605.64 μs | 2,533.732 μs | 7,350.817 μs | 1000.0000 | - | - | 5,826 KB | +| Method | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated | +|--------------- |--------------:|------------:|------------:|----------:|---------:|---------:|-----------:| +| EmitIl | 3.370 us | 0.0283 us | 0.0251 us | 0.4196 | 0.2060 | 0.0305 | 3.48 KB | +| ExpressionTree | 131.847 us | 0.4957 us | 0.4394 us | 1.2207 | 0.4883 | - | 11.4 KB | +| Sigil | 130.371 us | 1.2540 us | 1.1730 us | 52.7344 | 13.6719 | - | 433.2 KB | +| Roslyn | 14,522.046 us | 263.8589 us | 246.8138 us | 1031.2500 | 343.7500 | 125.0000 | 7757.56 KB | ## Invocation of parser -| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Allocated | -|---------------- |------------:|----------:|----------:|------:|--------:|-------:|----------:| -| EmitIl | 374.7 ns | 7.75 ns | 22.36 ns | 1.02 | 0.08 | 0.0095 | 40 B | -| ExpressionTree | 378.1 ns | 7.56 ns | 20.57 ns | 1.03 | 0.08 | 0.0095 | 40 B | -| Reflection | 13,625.0 ns | 272.60 ns | 750.81 ns | 37.29 | 2.29 | 0.7782 | 3,256 B | -| Sigil | 378.9 ns | 7.69 ns | 21.06 ns | 1.03 | 0.07 | 0.0095 | 40 B | -| Roslyn | 404.2 ns | 7.55 ns | 17.80 ns | 1.10 | 0.07 | 0.0095 | 40 B | -| SourceGenerator | 384.4 ns | 7.79 ns | 21.46 ns | 1.05 | 0.08 | 0.0095 | 40 B | -| ManuallyWritten | 367.8 ns | 7.36 ns | 15.68 ns | 1.00 | 0.00 | 0.0095 | 40 B | +| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | +|---------------- |------------:|----------:|----------:|------:|--------:|-------:|----------:|------------:| +| EmitIl | 75.14 ns | 0.352 ns | 0.294 ns | 1.01 | 0.00 | 0.0048 | 40 B | 1.00 | +| ExpressionTree | 76.12 ns | 1.306 ns | 1.222 ns | 1.02 | 0.02 | 0.0048 | 40 B | 1.00 | +| Reflection | 1,084.40 ns | 21.704 ns | 25.837 ns | 14.52 | 0.34 | 0.0992 | 832 B | 20.80 | +| Sigil | 75.51 ns | 0.319 ns | 0.249 ns | 1.01 | 0.00 | 0.0048 | 40 B | 1.00 | +| Roslyn | 82.45 ns | 0.117 ns | 0.110 ns | 1.10 | 0.00 | 0.0048 | 40 B | 1.00 | +| SourceGenerator | 75.59 ns | 0.092 ns | 0.086 ns | 1.01 | 0.00 | 0.0048 | 40 B | 1.00 | +| ManuallyWritten | 74.66 ns | 0.142 ns | 0.133 ns | 1.00 | 0.00 | 0.0048 | 40 B | 1.00 | diff --git a/benchmark/run.cmd b/benchmark/run.cmd deleted file mode 100644 index 7c6096e..0000000 --- a/benchmark/run.cmd +++ /dev/null @@ -1,2 +0,0 @@ -dotnet build -c Release -dotnet bin\Release\net5.0\Parsers.Benchmarks.dll all \ No newline at end of file diff --git a/benchmark/run.sh b/benchmark/run.sh new file mode 100755 index 0000000..773b42c --- /dev/null +++ b/benchmark/run.sh @@ -0,0 +1 @@ +dotnet build -c Release && ./bin/Release/net9.0/Parsers.Benchmarks all \ No newline at end of file From 54f31498a8abbabfdf9ba8005d384db53acff00c Mon Sep 17 00:00:00 2001 From: Maxim Tkachenko Date: Mon, 13 Oct 2025 21:03:08 +0200 Subject: [PATCH 7/7] simplified ParserSourceGenerator --- .../ParserSourceGenerator.cs | 40 +++++-------------- 1 file changed, 11 insertions(+), 29 deletions(-) diff --git a/src/Parsers.SourceGenerator/ParserSourceGenerator.cs b/src/Parsers.SourceGenerator/ParserSourceGenerator.cs index f7e53bd..50fc73f 100644 --- a/src/Parsers.SourceGenerator/ParserSourceGenerator.cs +++ b/src/Parsers.SourceGenerator/ParserSourceGenerator.cs @@ -21,7 +21,7 @@ public class ParserSourceGenerator : IIncrementalGenerator { public void Initialize(IncrementalGeneratorInitializationContext context) { - var classDeclarations = context.SyntaxProvider + var classesToGenerateFor = context.SyntaxProvider .CreateSyntaxProvider( predicate: static (syntaxNode, _) => syntaxNode is ClassDeclarationSyntax { @@ -30,38 +30,20 @@ public void Initialize(IncrementalGeneratorInitializationContext context) transform: (ctx, _) => { var classDeclaration = (ClassDeclarationSyntax)ctx.Node; - return ctx.SemanticModel.GetDeclaredSymbol(classDeclaration) as INamedTypeSymbol; - }) - .Where(static symbol => symbol is not null); - - var parserOutputAttrSymbol = context.CompilationProvider - .Select((compilation, _) => compilation.GetTypeByMetadataName("Parsers.ParserOutputAttribute")); + var symbol = ctx.SemanticModel.GetDeclaredSymbol(classDeclaration) as INamedTypeSymbol; + if (symbol == null) return null; - var classSymbolsArray = classDeclarations - .Combine(parserOutputAttrSymbol) - .Select((l, _) => - { - var (symbol, parserAttr) = l; - if (parserAttr is null) return null; - - return symbol.GetAttributes().Any(a => - SymbolEqualityComparer.Default.Equals(a.AttributeClass, parserAttr)) - ? symbol - : null; - }) + return symbol.GetAttributes().Any(attr => attr.AttributeClass is { Name: "ParserOutputAttribute" }) + ? symbol + : null; + }) .Where(static symbol => symbol is not null) .Collect(); - context.RegisterSourceOutput( - context.CompilationProvider.Combine(classSymbolsArray), - (sourceProductionContext, f ) => - { - var attributeIndexTypeSymbol = f.Left.GetTypeByMetadataName("Parsers.ArrayIndexAttribute"); - GenerateSerializer(sourceProductionContext, f.Right, attributeIndexTypeSymbol); - }); + context.RegisterSourceOutput(classesToGenerateFor, GenerateSerializer); } - private static void GenerateSerializer(SourceProductionContext context, ImmutableArray classSymbol, INamedTypeSymbol attributeIndexTypeSymbol) + private static void GenerateSerializer(SourceProductionContext context, ImmutableArray classSymbol) { var typeNames = new List<(string TargetTypeName, string TargetTypeFullName, string TargetTypeParserName)>(); var builder = new StringBuilder(); @@ -89,8 +71,8 @@ public class Parser : IParserFactory var props = typeSymbol.GetMembers().OfType(); foreach (var prop in props) { - var attr = prop.GetAttributes().FirstOrDefault(x => SymbolEqualityComparer.Default.Equals(x.AttributeClass, attributeIndexTypeSymbol)); - if (attr == null || attr.ConstructorArguments[0].Value is not int) continue; + var attr = prop.GetAttributes().FirstOrDefault(attr => attr.AttributeClass is { Name: "ArrayIndexAttribute" }); + if (attr?.ConstructorArguments[0].Value is not int) continue; int order = (int) attr.ConstructorArguments[0].Value; if (order < 0) continue;