From 5fa8e5d453a3a5529041c106b30f93bc557110d2 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Sun, 4 Aug 2024 21:20:19 +0100 Subject: [PATCH 01/14] start implementing new syntax builder --- .../ApiRequestSourceBuilder.cs | 109 ++++++++++++++++++ .../DragonFruit.Data.Roslyn.csproj | 1 + 2 files changed, 110 insertions(+) create mode 100644 DragonFruit.Data.Roslyn/ApiRequestSourceBuilder.cs diff --git a/DragonFruit.Data.Roslyn/ApiRequestSourceBuilder.cs b/DragonFruit.Data.Roslyn/ApiRequestSourceBuilder.cs new file mode 100644 index 0000000..1aa90df --- /dev/null +++ b/DragonFruit.Data.Roslyn/ApiRequestSourceBuilder.cs @@ -0,0 +1,109 @@ +// DragonFruit.Data Copyright DragonFruit Network +// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details + +using System.Linq; +using System.Text; +using DragonFruit.Data.Requests; +using DragonFruit.Data.Roslyn.Entities; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using StrawberryShake.CodeGeneration; +using StrawberryShake.CodeGeneration.CSharp.Builders; + +namespace DragonFruit.Data.Roslyn; + +internal static class ApiRequestSourceBuilder +{ + private static readonly UsingDirectiveSyntax[] DefaultUsingStatements = + [ + SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("System")), + SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("System.Text")), + SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("System.Net.Http")), + SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("DragonFruit.Data")), + SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("DragonFruit.Data.Requests")) + ]; + + public static CompilationUnitSyntax Build(INamedTypeSymbol classSymbol, RequestSymbolMetadata metadata) + { + // classes are partial by default + var classBuilder = new ClassBuilder() + .SetName(classSymbol.Name) + .AddImplements("global::DragonFruit.Data.Requests.IRequestBuilder"); + + var serializerMethodParamBuilder = new ParameterBuilder() + .SetType("global::DragonFruit.Data.Serializers.SerializerResolver") + .SetName("serializerResolver"); + + var methodBuilder = new MethodBuilder() + .SetName("BuildRequest") + .SetReturnType("global::System.Net.Http.HttpRequestMessage") + .SetAccessModifier(AccessModifier.Public) + .AddParameter(serializerMethodParamBuilder); + + // create a new UriBuilder called uriBuilder pulling in the uri from this.RequestPath + var methodBodyBuilder = new CodeBlockBuilder() + .AddCode("var uriBuilder = new global::System.UriBuilder(this.RequestPath);") + .AddEmptyLine(); + + // process queries + if (metadata.Properties[ParameterType.Query].Any()) + { + methodBodyBuilder.AddCode("var queryBuilder = new global::System.Text.StringBuilder();"); + methodBodyBuilder.AddEmptyLine(); + + foreach (var querySymbol in metadata.Properties[ParameterType.Query].OfType()) + { + var codeBlock = querySymbol switch + { + EnumerableSymbolMetadata enumerableSymbol => $"global::DragonFruit.Data.Converters.EnumerableConverter.WriteEnumerable(queryBuilder, {querySymbol.Accessor}, global::DragonFruit.Data.Requests.EnumerableOption.{enumerableSymbol.EnumerableOption}, \"{querySymbol.ParameterName}\", \"{enumerableSymbol.Separator}\");", + EnumSymbolMetadata enumSymbol => $"global::DragonFruit.Data.Converters.EnumConverter.WriteEnum(queryBuilder, {querySymbol.Accessor}, global::DragonFruit.Data.Requests.EnumOption.{enumSymbol.EnumOption}, \"{querySymbol.ParameterName}\");", + KeyValuePairSymbolMetadata => $"global::DragonFruit.Data.Converters.KeyValuePairConverter.WriteKeyValuePairs(queryBuilder, {querySymbol.Accessor});", + + _ => $"queryBuilder.AppendFormat(\"{{0}}={{1}}&\", \"{querySymbol.ParameterName}\", global::System.Uri.EscapeDataString({querySymbol.Accessor}.ToString()));" + }; + + if (querySymbol.Nullable) + { + methodBodyBuilder.AddCode(new IfBuilder().SetCondition($"{querySymbol.Accessor} != null").AddCode(codeBlock)).AddEmptyLine(); + } + else + { + methodBodyBuilder.AddCode(codeBlock).AddEmptyLine(); + } + } + + // remove trailing &, set query string + methodBodyBuilder.AddCode(new IfBuilder().SetCondition("queryBuilder.Length > 0") + .AddCode("queryBuilder.Length--;") + .AddCode("uriBuilder.Query = queryBuilder.ToString();")); + methodBodyBuilder.AddEmptyLine(); + methodBodyBuilder.AddEmptyLine(); + } + + // create request body + methodBodyBuilder.AddCode("var request = new global::System.Net.Http.HttpRequestMessage(this.RequestMethod, uriBuilder.Uri);"); + + methodBuilder.AddCode(methodBodyBuilder); + classBuilder.AddMethod(methodBuilder); + + var code = new StringBuilder(5000); + + using (var writer = new CodeWriter(code)) + { + classBuilder.Build(writer); + } + + var compilationUnit = SyntaxFactory.CompilationUnit(); + + compilationUnit = compilationUnit.AddUsings(DefaultUsingStatements); + compilationUnit = compilationUnit.AddMembers(SyntaxFactory.NamespaceDeclaration(SyntaxFactory.ParseName(classSymbol.ContainingNamespace.ToDisplayString()))); + + // merge sourceText and compilationUnit + var sourceText = SourceText.From(code.ToString()); + var tree = CSharpSyntaxTree.ParseText(sourceText); + + return compilationUnit.AddMembers(tree.GetRoot().DescendantNodes().OfType().ToArray()); + } +} diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj index 6849f8c..c61a389 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj @@ -54,6 +54,7 @@ + From 59e630b42f27adeaeac9e424cb7f5197955657a5 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Thu, 8 Aug 2024 16:01:14 +0100 Subject: [PATCH 02/14] add remaining source generator, using strings for uris --- .../ApiRequestSourceBuilder.cs | 223 +++++++++++++++--- .../ReflectionRequestMessageBuilder.cs | 6 +- 2 files changed, 194 insertions(+), 35 deletions(-) diff --git a/DragonFruit.Data.Roslyn/ApiRequestSourceBuilder.cs b/DragonFruit.Data.Roslyn/ApiRequestSourceBuilder.cs index 1aa90df..825a630 100644 --- a/DragonFruit.Data.Roslyn/ApiRequestSourceBuilder.cs +++ b/DragonFruit.Data.Roslyn/ApiRequestSourceBuilder.cs @@ -1,10 +1,12 @@ // DragonFruit.Data Copyright DragonFruit Network // Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details +using System.Collections.Generic; using System.Linq; using System.Text; using DragonFruit.Data.Requests; using DragonFruit.Data.Roslyn.Entities; +using DragonFruit.Data.Roslyn.Enums; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -25,7 +27,7 @@ internal static class ApiRequestSourceBuilder SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("DragonFruit.Data.Requests")) ]; - public static CompilationUnitSyntax Build(INamedTypeSymbol classSymbol, RequestSymbolMetadata metadata) + public static SourceText Build(INamedTypeSymbol classSymbol, RequestSymbolMetadata metadata, RequestBodyType requestBodyType) { // classes are partial by default var classBuilder = new ClassBuilder() @@ -44,7 +46,7 @@ public static CompilationUnitSyntax Build(INamedTypeSymbol classSymbol, RequestS // create a new UriBuilder called uriBuilder pulling in the uri from this.RequestPath var methodBodyBuilder = new CodeBlockBuilder() - .AddCode("var uriBuilder = new global::System.UriBuilder(this.RequestPath);") + .AddCode("var uriBuilder = new global::System.StringBuilder(this.RequestPath);") .AddEmptyLine(); // process queries @@ -53,38 +55,164 @@ public static CompilationUnitSyntax Build(INamedTypeSymbol classSymbol, RequestS methodBodyBuilder.AddCode("var queryBuilder = new global::System.Text.StringBuilder();"); methodBodyBuilder.AddEmptyLine(); - foreach (var querySymbol in metadata.Properties[ParameterType.Query].OfType()) - { - var codeBlock = querySymbol switch - { - EnumerableSymbolMetadata enumerableSymbol => $"global::DragonFruit.Data.Converters.EnumerableConverter.WriteEnumerable(queryBuilder, {querySymbol.Accessor}, global::DragonFruit.Data.Requests.EnumerableOption.{enumerableSymbol.EnumerableOption}, \"{querySymbol.ParameterName}\", \"{enumerableSymbol.Separator}\");", - EnumSymbolMetadata enumSymbol => $"global::DragonFruit.Data.Converters.EnumConverter.WriteEnum(queryBuilder, {querySymbol.Accessor}, global::DragonFruit.Data.Requests.EnumOption.{enumSymbol.EnumOption}, \"{querySymbol.ParameterName}\");", - KeyValuePairSymbolMetadata => $"global::DragonFruit.Data.Converters.KeyValuePairConverter.WriteKeyValuePairs(queryBuilder, {querySymbol.Accessor});", + WriteUriQueryBuilder(methodBodyBuilder, metadata.Properties[ParameterType.Query].OfType(), "queryBuilder"); - _ => $"queryBuilder.AppendFormat(\"{{0}}={{1}}&\", \"{querySymbol.ParameterName}\", global::System.Uri.EscapeDataString({querySymbol.Accessor}.ToString()));" - }; - - if (querySymbol.Nullable) - { - methodBodyBuilder.AddCode(new IfBuilder().SetCondition($"{querySymbol.Accessor} != null").AddCode(codeBlock)).AddEmptyLine(); - } - else - { - methodBodyBuilder.AddCode(codeBlock).AddEmptyLine(); - } - } - - // remove trailing &, set query string methodBodyBuilder.AddCode(new IfBuilder().SetCondition("queryBuilder.Length > 0") .AddCode("queryBuilder.Length--;") - .AddCode("uriBuilder.Query = queryBuilder.ToString();")); + .AddCode("uriBuilder.Append(\"?\").Append(queryBuilder);")); methodBodyBuilder.AddEmptyLine(); methodBodyBuilder.AddEmptyLine(); } // create request body - methodBodyBuilder.AddCode("var request = new global::System.Net.Http.HttpRequestMessage(this.RequestMethod, uriBuilder.Uri);"); + methodBodyBuilder.AddCode("var request = new global::System.Net.Http.HttpRequestMessage(this.RequestMethod, uriBuilder.ToString());").AddEmptyLine(); + + // process body content + switch (requestBodyType) + { + case RequestBodyType.CustomBodyDirect: + methodBodyBuilder.AddCode($"request.Content = {metadata.BodyProperty.Accessor};"); + break; + + case RequestBodyType.CustomBodySerialized: + methodBodyBuilder.AddCode($"var requestContentData = {metadata.BodyProperty.Accessor}"); + methodBodyBuilder.AddCode(new IfBuilder().SetCondition("requestContentData != null") + .AddCode("request.Content = serializerResolver.Resolve(requestContentData.GetType(), global::DragonFruit.Data.Serializers.DataDirection.Out).Serialize(requestContentData);")); + break; + + case RequestBodyType.FormUriEncoded: + methodBodyBuilder.AddCode("var formContentBuilder = new global::System.Text.StringBuilder();"); + WriteUriQueryBuilder(methodBodyBuilder, metadata.Properties[ParameterType.Form].OfType(), "formContentBuilder"); + + methodBodyBuilder.AddCode(new IfBuilder().SetCondition("formContentBuilder.Length > 0").AddCode("formContentBuilder.Length--;")); + methodBodyBuilder.AddCode("request.Content = new global::System.Net.Http.StringContent(formContentBuilder.ToString(), global::System.Text.Encoding.UTF8, \"application/x-www-form-urlencoded\");"); + break; + + case RequestBodyType.FormMultipart: + methodBodyBuilder.AddCode("var multipartContent = new global::System.Net.Http.MultipartFormDataContent();"); + + int counter = 0; + + foreach (var symbol in metadata.Properties[ParameterType.Form].OfType()) + { + string variableName; + + switch (symbol.Type) + { + case RequestSymbolType.ByteArray: + { + variableName = $"ba{++counter}"; + methodBodyBuilder.AddCode($"var {variableName} = {symbol.Accessor};"); + methodBodyBuilder.AddCode(new IfBuilder().SetCondition($"{variableName} != null").AddCode($"multipartContent.Add(new global::System.Net.Http.ByteArrayContent({variableName}), \"{symbol.ParameterName}\");")); + break; + } + + case RequestSymbolType.Stream: + { + variableName = $"st{++counter}"; + methodBodyBuilder.AddCode($"var {variableName} = {symbol.Accessor}"); + methodBodyBuilder.AddCode(new IfBuilder().SetCondition($"{variableName} != null").AddCode($"multipartContent.Add(new global::System.Net.Http.StreamContent({variableName}), \"{symbol.ParameterName}\");")); + break; + } + + case RequestSymbolType.Enum when symbol is EnumSymbolMetadata enumSymbol: + { + var line = $"global::System.Net.Http.StringContent(global::DragonFruit.Data.Converters.EnumConverter.GetEnumValue({symbol.Accessor}, global::DragonFruit.Data.Requests.EnumOption.{enumSymbol.EnumOption})), \"{symbol.ParameterName};\");"; + + if (symbol.Nullable) + { + methodBodyBuilder.AddCode(new IfBuilder().SetCondition($"{symbol.Accessor} != null").AddCode(line)); + } + else + { + methodBodyBuilder.AddCode(line); + } + + break; + } + + case RequestSymbolType.Enumerable when symbol is EnumerableSymbolMetadata enumerableSymbol: + { + if (enumerableSymbol.IsByteArray) + { + goto case RequestSymbolType.ByteArray; + } + + var enumerableBlock = new ForEachBuilder() + .SetLoopHeader($"var item in global::DragonFruit.Data.Converters.EnumerableConverter.GetPairs({symbol.Accessor}, global::DragonFruit.Data.Requests.EnumerableOption.{enumerableSymbol.EnumerableOption})") + .AddCode("multipartContent.Add(new global::System.Net.Http.StringContent(item.Value), item.Key);"); + + methodBodyBuilder.AddCode(new IfBuilder().SetCondition($"{symbol.Accessor} != null").AddCode(enumerableBlock)); + break; + } + + case RequestSymbolType.KeyValuePair: + { + var keyValuePairBlock = new ForEachBuilder() + .SetLoopHeader($"var item in (global::System.Collections.Generic.IEnumerable>){symbol.Accessor}") + .AddCode("multipartContent.Add(new global::System.Net.Http.StringContent(item.Value), item.Key);"); + + methodBodyBuilder.AddCode(new IfBuilder().SetCondition($"{symbol.Accessor} != null").AddCode(keyValuePairBlock)); + break; + } + + default: + { + var line = $"multipartContent.Add(new global::System.Net.Http.StringContent({symbol.Accessor}), \"{symbol.ParameterName}\");"; + + if (symbol.Nullable) + { + methodBodyBuilder.AddCode(new IfBuilder().SetCondition($"{symbol.Accessor} != null").AddCode(line)); + } + else + { + methodBodyBuilder.AddCode(line); + } + break; + } + } + } + + methodBodyBuilder.AddCode("request.Content = multipartContent;"); + break; + } + + // process headers + if (metadata.Properties[ParameterType.Header].Any()) + { + methodBodyBuilder.AddEmptyLine().AddEmptyLine(); + + foreach (var symbol in metadata.Properties[ParameterType.Header].OfType()) + { + var headerHandler = new IfBuilder().SetCondition($"{symbol.Accessor} != null"); + + switch (symbol) + { + case EnumerableSymbolMetadata enumerableSymbol: + headerHandler.AddCode(new ForEachBuilder().SetLoopHeader($"var kvp in global::DragonFruit.Data.Converters.EnumerableConverter.GetPairs({symbol.Accessor}, global::DragonFruit.Data.Requests.EnumerableOption.{enumerableSymbol.EnumerableOption}, \"{symbol.ParameterName}\", \"{enumerableSymbol.Separator}\")") + .AddCode("request.Headers.Add(kvp.Key, kvp.Value);")); + break; + + case KeyValuePairSymbolMetadata: + headerHandler.AddCode(new ForEachBuilder().SetLoopHeader($"var kvp in (global::System.Collections.Generic.IEnumerable>){symbol.Accessor}") + .AddCode("request.Headers.Add(kvp.Key, kvp.Value);")); + break; + + case EnumSymbolMetadata enumSymbol: + headerHandler.AddCode($"request.Headers.Add(\"{symbol.ParameterName}\", global::DragonFruit.Data.Converters.EnumConverter.GetEnumValue({symbol.Accessor}, global::DragonFruit.Data.Requests.EnumOption.{enumSymbol.EnumOption}));"); + break; + + default: + headerHandler.AddCode($"request.Headers.Add(\"{symbol.ParameterName}\", {symbol.Accessor}.ToString());"); + break; + } + + methodBodyBuilder.AddCode(headerHandler).AddEmptyLine(); + } + } + + methodBodyBuilder.AddCode("return request;"); methodBuilder.AddCode(methodBodyBuilder); classBuilder.AddMethod(methodBuilder); @@ -95,15 +223,46 @@ public static CompilationUnitSyntax Build(INamedTypeSymbol classSymbol, RequestS classBuilder.Build(writer); } - var compilationUnit = SyntaxFactory.CompilationUnit(); + // add class with generated code annotation + var tree = CSharpSyntaxTree.ParseText(SourceText.From(code.ToString())); + var classNode = tree.GetRoot().DescendantNodes().OfType().Single(); + var compilerGeneratedAttribute = SyntaxFactory.Attribute(SyntaxFactory.ParseName("System.CodeDom.Compiler.GeneratedCodeAttribute")).WithArgumentList(SyntaxFactory.AttributeArgumentList(SyntaxFactory.SeparatedList(new[] + { + SyntaxFactory.AttributeArgument(SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal("DragonFruit.Data"))), + SyntaxFactory.AttributeArgument(SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal("4.1.0"))) + }))); + + classNode = classNode.AddAttributeLists(SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(compilerGeneratedAttribute))); - compilationUnit = compilationUnit.AddUsings(DefaultUsingStatements); - compilationUnit = compilationUnit.AddMembers(SyntaxFactory.NamespaceDeclaration(SyntaxFactory.ParseName(classSymbol.ContainingNamespace.ToDisplayString()))); + return SyntaxFactory.CompilationUnit() + .AddUsings(DefaultUsingStatements) + .AddMembers(SyntaxFactory.NamespaceDeclaration(SyntaxFactory.ParseName(classSymbol.ContainingNamespace.ToDisplayString()))) + .AddMembers(classNode) + .WithLeadingTrivia(SyntaxFactory.Comment("// ")) + .GetText(); + } + + private static void WriteUriQueryBuilder(CodeBlockBuilder builder, IEnumerable symbols, string builderName) + { + foreach (var querySymbol in symbols) + { + var codeBlock = querySymbol switch + { + EnumerableSymbolMetadata enumerableSymbol => $"global::DragonFruit.Data.Converters.EnumerableConverter.WriteEnumerable({builderName}, {querySymbol.Accessor}, global::DragonFruit.Data.Requests.EnumerableOption.{enumerableSymbol.EnumerableOption}, \"{querySymbol.ParameterName}\", \"{enumerableSymbol.Separator}\");", + EnumSymbolMetadata enumSymbol => $"global::DragonFruit.Data.Converters.EnumConverter.WriteEnum({builderName}, {querySymbol.Accessor}, global::DragonFruit.Data.Requests.EnumOption.{enumSymbol.EnumOption}, \"{querySymbol.ParameterName}\");", + KeyValuePairSymbolMetadata => $"global::DragonFruit.Data.Converters.KeyValuePairConverter.WriteKeyValuePairs({builderName}, {querySymbol.Accessor});", - // merge sourceText and compilationUnit - var sourceText = SourceText.From(code.ToString()); - var tree = CSharpSyntaxTree.ParseText(sourceText); + _ => $"{builderName}.AppendFormat(\"{{0}}={{1}}&\", \"{querySymbol.ParameterName}\", global::System.Uri.EscapeDataString({querySymbol.Accessor}.ToString()));" + }; - return compilationUnit.AddMembers(tree.GetRoot().DescendantNodes().OfType().ToArray()); + if (querySymbol.Nullable) + { + builder.AddCode(new IfBuilder().SetCondition($"{querySymbol.Accessor} != null").AddCode(codeBlock)).AddEmptyLine(); + } + else + { + builder.AddCode(codeBlock).AddEmptyLine(); + } + } } } diff --git a/DragonFruit.Data/Converters/ReflectionRequestMessageBuilder.cs b/DragonFruit.Data/Converters/ReflectionRequestMessageBuilder.cs index 6e94f8c..6776ca4 100644 --- a/DragonFruit.Data/Converters/ReflectionRequestMessageBuilder.cs +++ b/DragonFruit.Data/Converters/ReflectionRequestMessageBuilder.cs @@ -23,7 +23,7 @@ public static HttpRequestMessage CreateHttpRequestMessage(ApiRequest request, Se var requestProperties = requestType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy); var requestParams = requestProperties.Select(GetPropertyInfo).Where(x => x != null).ToLookup(x => x.Value.ParameterType, x => (PropertyName: x.Value.ParameterName, x.Value.Accessor)); - var requestUri = new UriBuilder(request.RequestPath); + var requestUri = new StringBuilder(request.RequestPath); // build query if (requestParams[ParameterType.Query].Any()) @@ -39,11 +39,11 @@ public static HttpRequestMessage CreateHttpRequestMessage(ApiRequest request, Se { // trim trailing & queryBuilder.Length--; - requestUri.Query = queryBuilder.ToString(); + requestUri.Append('?').Append(queryBuilder); } } - var requestMessage = new HttpRequestMessage(request.RequestMethod, requestUri.Uri); + var requestMessage = new HttpRequestMessage(request.RequestMethod, requestUri.ToString()); // add headers foreach (var headerParameter in requestParams[ParameterType.Header]) From 15638483c4b0cca6cd06743a3afc493d57f2783e Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Thu, 8 Aug 2024 16:01:29 +0100 Subject: [PATCH 03/14] refactor metadata type name to better convey meaning --- DragonFruit.Data.Roslyn/Entities/EnumSymbolMetadata.cs | 2 +- DragonFruit.Data.Roslyn/Entities/EnumerableSymbolMetadata.cs | 2 +- .../Entities/KeyValuePairSymbolMetadata.cs | 2 +- .../{PropertySymbolMetadata.cs => ParameterSymbolMetadata.cs} | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) rename DragonFruit.Data.Roslyn/Entities/{PropertySymbolMetadata.cs => ParameterSymbolMetadata.cs} (70%) diff --git a/DragonFruit.Data.Roslyn/Entities/EnumSymbolMetadata.cs b/DragonFruit.Data.Roslyn/Entities/EnumSymbolMetadata.cs index 62c8d44..b6ced13 100644 --- a/DragonFruit.Data.Roslyn/Entities/EnumSymbolMetadata.cs +++ b/DragonFruit.Data.Roslyn/Entities/EnumSymbolMetadata.cs @@ -7,7 +7,7 @@ namespace DragonFruit.Data.Roslyn.Entities { - internal class EnumSymbolMetadata : PropertySymbolMetadata + internal class EnumSymbolMetadata : ParameterSymbolMetadata { public override RequestSymbolType Type => RequestSymbolType.Enum; diff --git a/DragonFruit.Data.Roslyn/Entities/EnumerableSymbolMetadata.cs b/DragonFruit.Data.Roslyn/Entities/EnumerableSymbolMetadata.cs index a8d5808..c543e03 100644 --- a/DragonFruit.Data.Roslyn/Entities/EnumerableSymbolMetadata.cs +++ b/DragonFruit.Data.Roslyn/Entities/EnumerableSymbolMetadata.cs @@ -7,7 +7,7 @@ namespace DragonFruit.Data.Roslyn.Entities { - internal class EnumerableSymbolMetadata : PropertySymbolMetadata + internal class EnumerableSymbolMetadata : ParameterSymbolMetadata { public override RequestSymbolType Type => RequestSymbolType.Enumerable; diff --git a/DragonFruit.Data.Roslyn/Entities/KeyValuePairSymbolMetadata.cs b/DragonFruit.Data.Roslyn/Entities/KeyValuePairSymbolMetadata.cs index 546ec95..72a8d2a 100644 --- a/DragonFruit.Data.Roslyn/Entities/KeyValuePairSymbolMetadata.cs +++ b/DragonFruit.Data.Roslyn/Entities/KeyValuePairSymbolMetadata.cs @@ -6,7 +6,7 @@ namespace DragonFruit.Data.Roslyn.Entities { - internal class KeyValuePairSymbolMetadata : PropertySymbolMetadata + internal class KeyValuePairSymbolMetadata : ParameterSymbolMetadata { public override RequestSymbolType Type => RequestSymbolType.KeyValuePair; diff --git a/DragonFruit.Data.Roslyn/Entities/PropertySymbolMetadata.cs b/DragonFruit.Data.Roslyn/Entities/ParameterSymbolMetadata.cs similarity index 70% rename from DragonFruit.Data.Roslyn/Entities/PropertySymbolMetadata.cs rename to DragonFruit.Data.Roslyn/Entities/ParameterSymbolMetadata.cs index b9a5b03..5d5a5a6 100644 --- a/DragonFruit.Data.Roslyn/Entities/PropertySymbolMetadata.cs +++ b/DragonFruit.Data.Roslyn/Entities/ParameterSymbolMetadata.cs @@ -6,11 +6,11 @@ namespace DragonFruit.Data.Roslyn.Entities { - internal class PropertySymbolMetadata : SymbolMetadata + internal class ParameterSymbolMetadata : SymbolMetadata { public virtual RequestSymbolType Type { get; } - public PropertySymbolMetadata(ISymbol symbol, ITypeSymbol returnType, string parameterName, RequestSymbolType type = RequestSymbolType.Standard) + public ParameterSymbolMetadata(ISymbol symbol, ITypeSymbol returnType, string parameterName, RequestSymbolType type = RequestSymbolType.Standard) : base(symbol, returnType) { Type = type; From d78b1e5e60bbf7a6aa4ec74eea0d882ef1b62469 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Thu, 8 Aug 2024 16:02:10 +0100 Subject: [PATCH 04/14] switch sourcegenerators out --- .../ApiRequestSourceGenerator.cs | 60 ++----------------- 1 file changed, 5 insertions(+), 55 deletions(-) diff --git a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs index 7baba03..7446a40 100644 --- a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs +++ b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs @@ -8,7 +8,6 @@ using System.IO; using System.Linq; using System.Net.Http; -using System.Text; using DragonFruit.Data.Converters; using DragonFruit.Data.Requests; using DragonFruit.Data.Roslyn.Entities; @@ -16,16 +15,12 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Text; -using Scriban; namespace DragonFruit.Data.Roslyn { [Generator(LanguageNames.CSharp)] public class ApiRequestSourceGenerator : IIncrementalGenerator { - public static readonly string TemplateName = "DragonFruit.Data.Roslyn.Templates.ApiRequest.liquid"; - private static readonly HashSet SupportedCollectionTypes = [ ..new[] @@ -40,8 +35,6 @@ public class ApiRequestSourceGenerator : IIncrementalGenerator } ]; - private Template _partialRequestTemplate; - static ApiRequestSourceGenerator() { ExternalDependencyLoader.RegisterDependencyLoader(); @@ -49,16 +42,6 @@ static ApiRequestSourceGenerator() public void Initialize(IncrementalGeneratorInitializationContext context) { - using var templateStream = typeof(ApiRequestSourceGenerator).Assembly.GetManifestResourceStream(TemplateName); - - if (templateStream == null) - { - throw new NullReferenceException("Could not find template"); - } - - using var reader = new StreamReader(templateStream); - _partialRequestTemplate = Template.ParseLiquid(reader.ReadToEnd()); - var apiRequestDerivedClasses = context.SyntaxProvider.CreateSyntaxProvider( predicate: (syntaxNode, _) => syntaxNode is ClassDeclarationSyntax classDecl && classDecl.Modifiers.Any(x => x.IsKind(SyntaxKind.PartialKeyword)), transform: (generatorSyntaxContext, _) => GetSemanticTarget(generatorSyntaxContext)); @@ -81,7 +64,6 @@ private void Execute(Compilation compilation, ImmutableArray"); var metadata = GetRequestSymbolMetadata(compilation, classSymbol); // check if body is derived from httpcontent @@ -97,39 +79,7 @@ private void Execute(Compilation compilation, ImmutableArray x.Name))); - genericNameBuilder.Append(">"); - - className = genericNameBuilder.ToString(); - } - - // create template info object - var parameterInfo = new - { - ClassName = className, - Namespace = classSymbol.ContainingNamespace.ToDisplayString(), - - RequireNewKeyword = WillHideOtherMembers(classSymbol, compilation.GetTypeByMetadataName(typeof(ApiRequest).FullName)), - - RequestBodyType = requestBodyType, - RequestBodySymbol = metadata.Properties[ParameterType.Form].Any() ? null : metadata.BodyProperty, // prefer using forms over body - this to match reflection-based behaviour - - QueryParameters = metadata.Properties[ParameterType.Query], - HeaderParameters = metadata.Properties[ParameterType.Header], - FormBodyParameters = metadata.Properties[ParameterType.Form], - }; - - sourceBuilder.Append("\n\n"); - sourceBuilder.Append(_partialRequestTemplate.Render(parameterInfo)); - context.AddSource($"{classSymbol.Name}.g.cs", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8)); + context.AddSource($"{classSymbol.Name}.dragonfruit.g.cs", ApiRequestSourceBuilder.Build(classSymbol, metadata, requestBodyType)); } } @@ -286,12 +236,12 @@ private static RequestSymbolMetadata GetRequestSymbolMetadata(Compilation compil // string (IEnumerable) else if (returnType.SpecialType == SpecialType.System_String) { - symbolMetadata = new PropertySymbolMetadata(candidate, returnType, parameterName); + symbolMetadata = new ParameterSymbolMetadata(candidate, returnType, parameterName); } // Stream else if (streamTypeSymbol.Equals(returnType, SymbolEqualityComparer.Default) || DerivesFrom(returnType, streamTypeSymbol)) { - symbolMetadata = new PropertySymbolMetadata(candidate, returnType, parameterName, RequestSymbolType.Stream); + symbolMetadata = new ParameterSymbolMetadata(candidate, returnType, parameterName, RequestSymbolType.Stream); } else { @@ -300,7 +250,7 @@ private static RequestSymbolMetadata GetRequestSymbolMetadata(Compilation compil { // byte[] case true when returnType is IArrayTypeSymbol { ElementType.SpecialType: SpecialType.System_Byte }: - symbolMetadata = new PropertySymbolMetadata(candidate, returnType, parameterName, RequestSymbolType.ByteArray); + symbolMetadata = new ParameterSymbolMetadata(candidate, returnType, parameterName, RequestSymbolType.ByteArray); break; // IEnumerable> @@ -322,7 +272,7 @@ private static RequestSymbolMetadata GetRequestSymbolMetadata(Compilation compil } default: - symbolMetadata = new PropertySymbolMetadata(candidate, returnType, parameterName); + symbolMetadata = new ParameterSymbolMetadata(candidate, returnType, parameterName); break; } } From c4cf6c59ce79522bb48b6583e571e1c5336e7584 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Thu, 8 Aug 2024 16:02:21 +0100 Subject: [PATCH 05/14] remove old liquid templates --- .../ApiRequestTemplateTests.cs | 31 ---- .../Templates/ApiRequest.liquid | 175 ------------------ 2 files changed, 206 deletions(-) delete mode 100644 DragonFruit.Data.Roslyn.Tests/ApiRequestTemplateTests.cs delete mode 100644 DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid diff --git a/DragonFruit.Data.Roslyn.Tests/ApiRequestTemplateTests.cs b/DragonFruit.Data.Roslyn.Tests/ApiRequestTemplateTests.cs deleted file mode 100644 index cb4c57e..0000000 --- a/DragonFruit.Data.Roslyn.Tests/ApiRequestTemplateTests.cs +++ /dev/null @@ -1,31 +0,0 @@ -// DragonFruit.Data Copyright DragonFruit Network -// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details - -using System.IO; -using System.Threading.Tasks; -using Scriban; -using Xunit; - -namespace DragonFruit.Data.Roslyn.Tests -{ - public class ApiRequestTemplateTests - { - [Fact] - public async Task TestTemplateParse() - { - var assembly = typeof(ApiRequestSourceGenerator).Assembly; - using var template = assembly.GetManifestResourceStream(ApiRequestSourceGenerator.TemplateName); - - Assert.NotNull(template); - - using var templateReader = new StreamReader(template); - var templateText = await templateReader.ReadToEndAsync(); - - Assert.True(templateText.Length > 0); - - var templateAst = Template.ParseLiquid(templateText); - - Assert.False(templateAst.HasErrors); - } - } -} diff --git a/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid b/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid deleted file mode 100644 index 937bfe3..0000000 --- a/DragonFruit.Data.Roslyn/Templates/ApiRequest.liquid +++ /dev/null @@ -1,175 +0,0 @@ -using System; -using System.Text; -using System.Net.Http; -using DragonFruit.Data; -using DragonFruit.Data.Requests; - -namespace {{ namespace }} -{ - partial class {{ class_name }} : global::DragonFruit.Data.Requests.IRequestBuilder - { - public {% if require_new_keyword %}new{% endif %} global::System.Net.Http.HttpRequestMessage BuildRequest(global::DragonFruit.Data.Serializers.SerializerResolver serializerResolver) - { - global::System.UriBuilder uriBuilder = new global::System.UriBuilder(this.RequestPath); - - {% comment %} Process Query Parameters {% endcomment %} - {% if query_parameters.size > 0 -%} - global::System.Text.StringBuilder queryBuilder = new global::System.Text.StringBuilder(); - - {% for query in query_parameters -%} - {% capture query_append -%} - {% case query.type -%} - {% when 1 -%} - global::DragonFruit.Data.Converters.EnumerableConverter.WriteEnumerable(queryBuilder, {{ query.accessor }}, global::DragonFruit.Data.Requests.EnumerableOption.{{ query.enumerable_option }}, "{{ query.parameter_name }}", "{{ query.separator }}"); - {% when 2 -%} - global::DragonFruit.Data.Converters.EnumConverter.WriteEnum(queryBuilder, {{ query.accessor }}, global::DragonFruit.Data.Requests.EnumOption.{{ query.enum_option }}, "{{ query.parameter_name }}"); - {% when 3 -%} - global::DragonFruit.Data.Converters.KeyValuePairConverter.WriteKeyValuePairs(queryBuilder, {{ query.accessor }}); - {% else -%} - queryBuilder.AppendFormat("{0}={1}&", "{{ query.parameter_name }}", global::System.Uri.EscapeDataString({{ query.accessor }}.ToString())); - {% endcase -%} - {% endcapture -%} - {% if query.nullable %} - if ({{ query.accessor }} != null) - { - {{ query_append }} - } - {% else -%} - {{ query_append }} - {% endif -%} - {% endfor -%} - - {% comment %} remove trailing & {% endcomment %} - if (queryBuilder.Length > 0) - { - queryBuilder.Length--; - uriBuilder.Query = queryBuilder.ToString(); - } - {% endif %} - - global::System.Net.Http.HttpRequestMessage request = new global::System.Net.Http.HttpRequestMessage(this.RequestMethod, uriBuilder.Uri); - - {% case request_body_type -%} - {% comment %} 1 - Multipart {% endcomment %} - {% when 1 -%} - global::System.Net.Http.MultipartFormDataContent content = new global::System.Net.Http.MultipartFormDataContent(); - - {% for multipart in form_body_parameters -%} - {% capture multipart_append -%} - {% case multipart.type -%} - {% when 1 -%} - foreach (var kvp in global::DragonFruit.Data.Converters.EnumerableConverter.GetPairs({{ multipart.accessor }}, global::DragonFruit.Data.Requests.EnumerableOption.{{ multipart.enumerable_option }})) - { - content.Add(new global::System.Net.Http.StringContent(kvp.Value), "{{ multipart.parameter_name }}"); - } - {% when 2 -%} - content.Add(new global::System.Net.Http.StringContent(global::DragonFruit.Data.Converters.EnumConverter.GetEnumValue({{ multipart.accessor }}, global::DragonFruit.Data.Requests.EnumOption.{{ multipart.enum_option }})), "{{ multipart.parameter_name }}"); - {% when 3 %} - foreach (var kvp in (global::System.Collections.Generic.IEnumerable>){{ multipart.accessor }}) - { - content.Add(new global::System.Net.Http.StringContent(kvp.Value), kvp.Key); - } - {% comment %} 4 - Stream {% endcomment %} - {% when 4 -%} - content.Add(new global::System.Net.Http.StreamContent({{ multipart.accessor }}), "{{ multipart.parameter_name }}"); - {% comment %} 5 - ByteArray {% endcomment %} - {% when 5 -%} - content.Add(new global::System.Net.Http.ByteArrayContent({{ multipart.accessor }}), "{{ multipart.parameter_name }}"); - {% comment %} Handle other types using ToString {% endcomment %} - {% else -%} - content.Add(new global::System.Net.Http.StringContent({{ multipart.accessor }}.ToString()), "{{ multipart.parameter_name }}"); - {% endcase -%} - {% endcapture -%} - - {% if multipart.nullable %} - if ({{ multipart.accessor }} != null) - { - {{ multipart_append }} - } - {% else -%} - {{ multipart_append }} - {% endif -%} - {% endfor -%} - - request.Content = content; - - {% comment %} 2 - UriEncoded {% endcomment %} - {% when 2 -%} - global::System.Text.StringBuilder formBuilder = new global::System.Text.StringBuilder(); - - {% for uriparam in form_body_parameters -%} - {% capture uriparam_append -%} - {% case uriparam.type -%} - {% when 1 -%} - global::DragonFruit.Data.Converters.EnumerableConverter.WriteEnumerable(formBuilder, {{ uriparam.accessor }}, global::DragonFruit.Data.Requests.EnumerableOption.{{ uriparam.enumerable_option }}, "{{ uriparam.parameter_name }}", "{{ uriparam.separator }}"); - {% when 2 -%} - global::DragonFruit.Data.Converters.EnumConverter.WriteEnum(formBuilder, {{ uriparam.accessor }}, global::DragonFruit.Data.Requests.EnumOption.{{ uriparam.enum_option }}, "{{ uriparam.parameter_name }}"); - {% when 3 -%} - global::DragonFruit.Data.Converters.KeyValuePairConverter.WriteKeyValuePairs(formBuilder, {{ uriparam.accessor }}); - {% else -%} - formBuilder.AppendFormat("{0}={1}&", "{{ uriparam.parameter_name }}", global::System.Uri.EscapeDataString({{ uriparam.accessor }}.ToString())); - {% endcase -%} - {% endcapture -%} - - {% if uriparam.nullable %} - if ({{ uriparam.accessor }} != null) - { - {{ uriparam_append }} - } - {% else -%} - {{ uriparam_append }} - {% endif -%} - {% endfor -%} - - {% comment %} remove trailing & {% endcomment %} - if (formBuilder.Length > 0) - { - formBuilder.Length--; - } - - request.Content = new global::System.Net.Http.StringContent(formBuilder.ToString(), global::System.Text.Encoding.UTF8, "application/x-www-form-urlencoded"); - - {% comment %} 3 - Custom Body (HttpContent) {% endcomment %} - {% when 3 -%} - request.Content = {{ request_body_symbol.accessor }}; - - {% comment %} 4 - Custom Body (Serialized) {% endcomment %} - {% when 4 -%} - request.Content = serializerResolver.Resolve({{ request_body_symbol.accessor }}.GetType(), global::DragonFruit.Data.Serializers.DataDirection.Out).Serialize({{ request_body_symbol.accessor }}); - {% endcase -%} - - {% comment %} Process Headers {% endcomment %} - {% for header in header_parameters -%} - {% capture header_append -%} - {% case header.type -%} - {% when 1 -%} - foreach (var kvp in global::DragonFruit.Data.Converters.EnumerableConverter.GetPairs({{ header.accessor }}, global::DragonFruit.Data.Requests.EnumerableOption.{{ header.enumerable_option }}, "{{ header.parameter_name }}", "{{ header.separator }}")) - { - request.Headers.Add(kvp.Key, kvp.Value); - } - {% when 2 -%} - request.Headers.Add("{{ header.parameter_name }}", global::DragonFruit.Data.Converters.EnumConverter.GetEnumValue({{ header.accessor }}, global::DragonFruit.Data.Requests.EnumOption.{{ header.enum_option }})); - {% when 3 -%} - foreach (var kvp in (global::System.Collections.Generic.IEnumerable>){{ header.accessor }}) - { - request.Headers.Add(kvp.Key, kvp.Value); - } - {% else -%} - request.Headers.Add("{{ header.parameter_name }}", {{ header.accessor }}.ToString()); - {% endcase -%} - {% endcapture -%} - - {% if header.nullable -%} - if ({{ header.accessor }} != null) - { - {{ header_append }} - } - {% else -%} - {{ header_append }} - {% endif -%} - {% endfor -%} - - return request; - } - } -} From d051341225601858567df3119b2957a19266c0e8 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Thu, 8 Aug 2024 16:02:34 +0100 Subject: [PATCH 06/14] remove scriban, embed sourcegen tool --- .../DragonFruit.Data.Roslyn.Tests.csproj | 2 +- DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/DragonFruit.Data.Roslyn.Tests/DragonFruit.Data.Roslyn.Tests.csproj b/DragonFruit.Data.Roslyn.Tests/DragonFruit.Data.Roslyn.Tests.csproj index 4fd1b49..21be28b 100644 --- a/DragonFruit.Data.Roslyn.Tests/DragonFruit.Data.Roslyn.Tests.csproj +++ b/DragonFruit.Data.Roslyn.Tests/DragonFruit.Data.Roslyn.Tests.csproj @@ -9,7 +9,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj index c61a389..bfabc17 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj @@ -38,7 +38,7 @@ - + @@ -53,8 +53,7 @@ - - + From 15c96963be9629a07951fce640a143fd45727d6a Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 9 Aug 2024 10:47:49 +0100 Subject: [PATCH 07/14] get outputs running --- DragonFruit.Data.Roslyn/ApiRequestSourceBuilder.cs | 2 +- .../ApiRequestSourceGenerator.cs | 5 ----- .../DragonFruit.Data.Roslyn.csproj | 14 ++++---------- DragonFruit.Data.sln.DotSettings | 7 +++++-- 4 files changed, 10 insertions(+), 18 deletions(-) diff --git a/DragonFruit.Data.Roslyn/ApiRequestSourceBuilder.cs b/DragonFruit.Data.Roslyn/ApiRequestSourceBuilder.cs index 825a630..88df67c 100644 --- a/DragonFruit.Data.Roslyn/ApiRequestSourceBuilder.cs +++ b/DragonFruit.Data.Roslyn/ApiRequestSourceBuilder.cs @@ -239,7 +239,7 @@ public static SourceText Build(INamedTypeSymbol classSymbol, RequestSymbolMetada .AddMembers(SyntaxFactory.NamespaceDeclaration(SyntaxFactory.ParseName(classSymbol.ContainingNamespace.ToDisplayString()))) .AddMembers(classNode) .WithLeadingTrivia(SyntaxFactory.Comment("// ")) - .GetText(); + .GetText(Encoding.UTF8); } private static void WriteUriQueryBuilder(CodeBlockBuilder builder, IEnumerable symbols, string builderName) diff --git a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs index 7446a40..24ca183 100644 --- a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs +++ b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs @@ -35,11 +35,6 @@ public class ApiRequestSourceGenerator : IIncrementalGenerator } ]; - static ApiRequestSourceGenerator() - { - ExternalDependencyLoader.RegisterDependencyLoader(); - } - public void Initialize(IncrementalGeneratorInitializationContext context) { var apiRequestDerivedClasses = context.SyntaxProvider.CreateSyntaxProvider( diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj index bfabc17..e1af6a4 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj @@ -7,11 +7,14 @@ false netstandard2.0 + CS8669,$(NoWarn) + DragonFruit.Data.Roslyn DragonFruit.Data.Roslyn false true + true false true @@ -30,19 +33,12 @@ - - - - - - - - + @@ -53,7 +49,5 @@ - - diff --git a/DragonFruit.Data.sln.DotSettings b/DragonFruit.Data.sln.DotSettings index 7d5ff9f..4478f0a 100644 --- a/DragonFruit.Data.sln.DotSettings +++ b/DragonFruit.Data.sln.DotSettings @@ -1,5 +1,8 @@  + True + True + ExplicitlyExcluded False WRAP_IF_LONG DragonFruit.Data Copyright DragonFruit Network @@ -8,8 +11,8 @@ Licensed under the MIT License. Please refer to the LICENSE file at the root of True True True - ExplicitlyExcluded - ExplicitlyExcluded + + SOLUTION WARNING WARNING From 90eec3fc250d99b2d08c94d99b3643a7cc53e121 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 9 Aug 2024 10:48:06 +0100 Subject: [PATCH 08/14] directly embed used generator segments to analyser --- .../CodeGeneration/.editorconfig | 5 + .../CodeGeneration/AccessModifier.cs | 9 + .../Builders/AbstractTypeBuilder.cs | 40 +++ .../CodeGeneration/Builders/ArrayBuilder.cs | 98 +++++++ .../Builders/AssignmentBuilder.cs | 122 ++++++++ .../Builders/CatchBlockBuilder.cs | 67 +++++ .../CodeGeneration/Builders/ClassBuilder.cs | 272 +++++++++++++++++ .../Builders/CodeBlockBuilder.cs | 75 +++++ .../Builders/CodeFileBuilder.cs | 109 +++++++ .../Builders/CodeInlineBlockBuilder.cs | 35 +++ .../Builders/CodeInlineBuilder.cs | 34 +++ .../Builders/CodeLineBuilder.cs | 58 ++++ .../Builders/ConditionBuilder.cs | 115 ++++++++ .../Builders/ConstructorBuilder.cs | 158 ++++++++++ .../CodeGeneration/Builders/EnumBuilder.cs | 110 +++++++ .../Builders/ExceptionBuilder.cs | 52 ++++ .../CodeGeneration/Builders/FieldBuilder.cs | 147 ++++++++++ .../CodeGeneration/Builders/ForEachBuilder.cs | 68 +++++ .../Builders/HashCodeBuilder.cs | 41 +++ .../CodeGeneration/Builders/ICode.cs | 3 + .../CodeGeneration/Builders/ICodeBuilder.cs | 6 + .../CodeGeneration/Builders/ICodeContainer.cs | 10 + .../CodeGeneration/Builders/ITypeBuilder.cs | 3 + .../CodeGeneration/Builders/IfBuilder.cs | 128 ++++++++ .../CodeGeneration/Builders/Inheritance.cs | 9 + .../Builders/InterfaceBuilder.cs | 139 +++++++++ .../CodeGeneration/Builders/LambdaBuilder.cs | 96 ++++++ .../CodeGeneration/Builders/MethodBuilder.cs | 245 ++++++++++++++++ .../Builders/MethodCallBuilder.cs | 275 ++++++++++++++++++ .../Builders/NullCheckBuilder.cs | 94 ++++++ .../Builders/ParameterBuilder.cs | 76 +++++ .../Builders/PropertyBuilder.cs | 174 +++++++++++ .../Builders/SwitchExpressionBuilder.cs | 133 +++++++++ .../Builders/TryCatchBuilder.cs | 51 ++++ .../CodeGeneration/Builders/TupleBuilder.cs | 106 +++++++ .../Builders/TupleBuilderExtensions.cs | 30 ++ .../Builders/TypeReferenceBuilder.cs | 126 ++++++++ .../Builders/XmlCommentBuilder.cs | 61 ++++ .../CodeGeneration/CodeGeneratorException.cs | 5 + .../CodeGeneration/CodeWriter.cs | 144 +++++++++ .../CodeGeneration/CodeWriterExtensions.cs | 33 +++ .../CodeGeneration/Keywords.cs | 142 +++++++++ .../CodeGeneration/ListExtensions.cs | 101 +++++++ .../CodeGeneration/RuntimeTypeInfo.cs | 100 +++++++ .../CodeGeneration/TypeNames.cs | 65 +++++ 45 files changed, 3970 insertions(+) create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/.editorconfig create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/AccessModifier.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/Builders/AbstractTypeBuilder.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/Builders/ArrayBuilder.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/Builders/AssignmentBuilder.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/Builders/CatchBlockBuilder.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/Builders/ClassBuilder.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/Builders/CodeBlockBuilder.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/Builders/CodeFileBuilder.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/Builders/CodeInlineBlockBuilder.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/Builders/CodeInlineBuilder.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/Builders/CodeLineBuilder.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/Builders/ConditionBuilder.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/Builders/ConstructorBuilder.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/Builders/EnumBuilder.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/Builders/ExceptionBuilder.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/Builders/FieldBuilder.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/Builders/ForEachBuilder.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/Builders/HashCodeBuilder.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/Builders/ICode.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/Builders/ICodeBuilder.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/Builders/ICodeContainer.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/Builders/ITypeBuilder.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/Builders/IfBuilder.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/Builders/Inheritance.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/Builders/InterfaceBuilder.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/Builders/LambdaBuilder.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/Builders/MethodBuilder.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/Builders/MethodCallBuilder.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/Builders/NullCheckBuilder.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/Builders/ParameterBuilder.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/Builders/PropertyBuilder.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/Builders/SwitchExpressionBuilder.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/Builders/TryCatchBuilder.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/Builders/TupleBuilder.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/Builders/TupleBuilderExtensions.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/Builders/TypeReferenceBuilder.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/Builders/XmlCommentBuilder.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/CodeGeneratorException.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/CodeWriter.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/CodeWriterExtensions.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/Keywords.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/ListExtensions.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/RuntimeTypeInfo.cs create mode 100644 DragonFruit.Data.Roslyn/CodeGeneration/TypeNames.cs diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/.editorconfig b/DragonFruit.Data.Roslyn/CodeGeneration/.editorconfig new file mode 100644 index 0000000..5a284b1 --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/.editorconfig @@ -0,0 +1,5 @@ +root = true + +[*.cs] +generated_code = true +dotnet_analyzer_diagnostic.severity = none \ No newline at end of file diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/AccessModifier.cs b/DragonFruit.Data.Roslyn/CodeGeneration/AccessModifier.cs new file mode 100644 index 0000000..2f23f59 --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/AccessModifier.cs @@ -0,0 +1,9 @@ +namespace StrawberryShake.CodeGeneration; + +public enum AccessModifier +{ + Public = 0, + Internal = 1, + Protected = 2, + Private = 3, +} diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/Builders/AbstractTypeBuilder.cs b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/AbstractTypeBuilder.cs new file mode 100644 index 0000000..812f102 --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/AbstractTypeBuilder.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; + +namespace StrawberryShake.CodeGeneration.CSharp.Builders; + +public abstract class AbstractTypeBuilder : ITypeBuilder +{ + protected List Properties { get; } = []; + + protected string? Name { get; private set; } + + protected List Implements { get; } = []; + + public abstract void Build(CodeWriter writer); + + protected void SetName(string name) + { + Name = name; + } + + public void AddProperty(PropertyBuilder property) + { + if (property is null) + { + throw new ArgumentNullException(nameof(property)); + } + + Properties.Add(property); + } + + public void AddImplements(string value) + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentException(nameof(value)); + } + + Implements.Add(value); + } +} diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/Builders/ArrayBuilder.cs b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/ArrayBuilder.cs new file mode 100644 index 0000000..9612bfe --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/ArrayBuilder.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; + +namespace StrawberryShake.CodeGeneration.CSharp.Builders; + +public class ArrayBuilder : ICode +{ + private string? _prefix; + private string? _type; + private bool _determineStatement = true; + private bool _setReturn; + private readonly List _assignment = []; + + private ArrayBuilder() + { + } + + public ArrayBuilder SetType(string type) + { + _type = type; + return this; + } + + public ArrayBuilder AddAssignment(ICode code) + { + _assignment.Add(code); + return this; + } + + public ArrayBuilder SetDetermineStatement(bool value) + { + _determineStatement = value; + return this; + } + + public ArrayBuilder SetPrefix(string prefix) + { + _prefix = prefix; + return this; + } + + public ArrayBuilder SetReturn(bool value = true) + { + _setReturn = value; + return this; + } + + public void Build(CodeWriter writer) + { + if (_type is null) + { + throw new ArgumentNullException(nameof(_type)); + } + + if (_determineStatement) + { + writer.WriteIndent(); + } + + if (_setReturn) + { + writer.Write("return "); + } + + writer.Write(_prefix); + + writer.Write("new "); + writer.Write(_type); + writer.Write("[] {"); + writer.WriteLine(); + + using (writer.IncreaseIndent()) + { + for (var i = 0; i < _assignment.Count; i++) + { + writer.WriteIndent(); + _assignment[i].Build(writer); + if (i != _assignment.Count - 1) + { + writer.Write(","); + writer.WriteLine(); + } + } + } + + writer.WriteLine(); + writer.WriteIndent(); + writer.Write("}"); + + if (_determineStatement) + { + writer.Write(";"); + writer.WriteLine(); + } + } + + public static ArrayBuilder New() => new ArrayBuilder(); +} diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/Builders/AssignmentBuilder.cs b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/AssignmentBuilder.cs new file mode 100644 index 0000000..dbff7cc --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/AssignmentBuilder.cs @@ -0,0 +1,122 @@ +using System; + +namespace StrawberryShake.CodeGeneration.CSharp.Builders; + +public class AssignmentBuilder : ICode +{ + private ICode? _leftHandSide; + private ICode? _rightHandSide; + private bool _assertNonNull; + private string? _nonNullAssertTypeNameOverride; + private string _operator = "="; + private ICode? _assertException; + + public static AssignmentBuilder New() => new(); + + public AssignmentBuilder SetLeftHandSide(ICode value) + { + _leftHandSide = value; + return this; + } + + public AssignmentBuilder SetLeftHandSide(string value) + { + _leftHandSide = new CodeInlineBuilder().SetText(value); + return this; + } + + public AssignmentBuilder SetOperator(string value) + { + _operator = value; + return this; + } + + public AssignmentBuilder SetRightHandSide(ICode value) + { + _rightHandSide = value; + return this; + } + + public AssignmentBuilder SetRightHandSide(string value) + { + _rightHandSide = new CodeInlineBuilder().SetText(value); + return this; + } + + public AssignmentBuilder AssertNonNull(string? nonNullAssertTypeNameOverride = null) + { + _nonNullAssertTypeNameOverride = nonNullAssertTypeNameOverride; + return SetAssertNonNull(true); + } + + public AssignmentBuilder SetAssertNonNull(bool value = true) + { + _assertNonNull = value; + return this; + } + + public AssignmentBuilder SetAssertException(string code) + { + _assertException = CodeInlineBuilder.From($"throw new {code}"); + return this; + } + + public AssignmentBuilder SetAssertException(ExceptionBuilder code) + { + _assertException = code; + return this; + } + + public void Build(CodeWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (_leftHandSide is null || _rightHandSide is null) + { + throw new CodeGeneratorException("Assignment statement is not complete."); + } + + writer.WriteIndent(); + _leftHandSide.Build(writer); + + writer.Write(" "); + writer.Write(_operator); + writer.Write(" "); + + _rightHandSide.Build(writer); + if (_assertNonNull) + { + writer.WriteLine(); + using (writer.IncreaseIndent()) + { + writer.WriteIndent(); + writer.Write(" ?? "); + + if (_assertException is null) + { + writer.Write($"throw new {TypeNames.ArgumentNullException}(nameof("); + if (_nonNullAssertTypeNameOverride is not null) + { + writer.Write(_nonNullAssertTypeNameOverride); + } + else + { + _rightHandSide.Build(writer); + } + + writer.Write("))"); + } + else + { + _assertException.Build(writer); + } + } + } + + writer.Write(";"); + writer.WriteLine(); + } +} diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/Builders/CatchBlockBuilder.cs b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/CatchBlockBuilder.cs new file mode 100644 index 0000000..ba081f2 --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/CatchBlockBuilder.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; + +namespace StrawberryShake.CodeGeneration.CSharp.Builders; + +public class CatchBlockBuilder : ICode +{ + private string? _exception; + private string? _exceptionVariable; + private readonly List _code = []; + + public static CatchBlockBuilder New() => new(); + + public CatchBlockBuilder AddCode(ICode code) + { + _code.Add(code); + return this; + } + + public CatchBlockBuilder SetExceptionVariable(string name) + { + if (_exception is null) + { + _exception = TypeNames.Exception; + } + + _exceptionVariable = name; + return this; + } + + public CatchBlockBuilder SetExceptionType(string typeName) + { + _exception = typeName; + return this; + } + + public void Build(CodeWriter writer) + { + writer.WriteIndent(); + writer.Write($"catch"); + if (_exception is not null) + { + writer.Write("("); + writer.Write(_exception); + + if (_exceptionVariable is not null) + { + writer.WriteSpace(); + writer.Write(_exceptionVariable); + } + + writer.Write(")"); + } + writer.WriteLine(); + + writer.WriteIndentedLine("{"); + + using (writer.IncreaseIndent()) + { + foreach (var code in _code) + { + code.Build(writer); + } + } + + writer.WriteIndentedLine("}"); + } +} diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/Builders/ClassBuilder.cs b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/ClassBuilder.cs new file mode 100644 index 0000000..38ca386 --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/ClassBuilder.cs @@ -0,0 +1,272 @@ +using System; +using System.Collections.Generic; + +namespace StrawberryShake.CodeGeneration.CSharp.Builders; + +public class ClassBuilder : AbstractTypeBuilder +{ + private AccessModifier _accessModifier; + private readonly bool _isPartial = true; + private bool _isStatic; + private bool _isSealed; + private bool _isAbstract; + private string? _name; + private XmlCommentBuilder? _xmlComment; + private readonly List _fields = []; + private readonly List _constructors = []; + private readonly List _methods = []; + private readonly List _classes = []; + + public static ClassBuilder New() => new(); + + public static ClassBuilder New(string className) => new ClassBuilder().SetName(className); + + public ClassBuilder SetAccessModifier(AccessModifier value) + { + _accessModifier = value; + return this; + } + + public new ClassBuilder SetName(string value) + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentException(nameof(value)); + } + + _name = value; + return this; + } + + public new ClassBuilder AddImplements(string value) + { + base.AddImplements(value); + return this; + } + + public ClassBuilder SetComment(string? xmlComment) + { + if (xmlComment is not null) + { + _xmlComment = XmlCommentBuilder.New().SetSummary(xmlComment); + } + + return this; + } + + public ClassBuilder SetComment(XmlCommentBuilder? xmlComment) + { + if (xmlComment is not null) + { + _xmlComment = xmlComment; + } + + return this; + } + + public ClassBuilder AddConstructor(ConstructorBuilder constructor) + { + if (constructor is null) + { + throw new ArgumentNullException(nameof(constructor)); + } + + _constructors.Add(constructor); + return this; + } + + public ClassBuilder AddField(FieldBuilder field) + { + if (field is null) + { + throw new ArgumentNullException(nameof(field)); + } + + _fields.Add(field); + return this; + } + + public new ClassBuilder AddProperty(PropertyBuilder property) + { + base.AddProperty(property); + return this; + } + + public ClassBuilder AddMethod(MethodBuilder method) + { + if (method is null) + { + throw new ArgumentNullException(nameof(method)); + } + + _methods.Add(method); + return this; + } + + public ClassBuilder AddClass(ClassBuilder classBuilder) + { + if (classBuilder is null) + { + throw new ArgumentNullException(nameof(classBuilder)); + } + + _classes.Add(classBuilder); + return this; + } + + public ClassBuilder AddClass(string @class) + { + if (@class is null) + { + throw new ArgumentNullException(nameof(@class)); + } + + _classes.Add(CodeInlineBuilder.From(@class)); + return this; + } + + public ClassBuilder SetStatic() + { + _isStatic = true; + _isSealed = false; + _isAbstract = false; + return this; + } + + public ClassBuilder SetSealed() + { + _isStatic = false; + _isSealed = true; + _isAbstract = false; + return this; + } + + public ClassBuilder SetAbstract() + { + _isStatic = false; + _isSealed = false; + _isAbstract = true; + return this; + } + + public override void Build(CodeWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + _xmlComment?.Build(writer); + + writer.WriteGeneratedAttribute(); + + var modifier = _accessModifier.ToString().ToLowerInvariant(); + + writer.WriteIndent(); + + writer.Write($"{modifier} "); + + if (_isStatic) + { + writer.Write("static "); + } + else if (_isSealed) + { + writer.Write("sealed "); + } + else if (_isAbstract) + { + writer.Write("abstract "); + } + + if (_isPartial) + { + writer.Write("partial "); + } + + writer.Write("class "); + writer.WriteLine(_name); + + if (!_isStatic && Implements.Count > 0) + { + using (writer.IncreaseIndent()) + { + for (var i = 0; i < Implements.Count; i++) + { + writer.WriteIndentedLine(i == 0 + ? $": {Implements[i]}" + : $", {Implements[i]}"); + } + } + } + + writer.WriteIndentedLine("{"); + + var writeLine = false; + + using (writer.IncreaseIndent()) + { + if (_fields.Count > 0) + { + foreach (var builder in _fields) + { + builder.Build(writer); + } + + writeLine = true; + } + + if (_constructors.Count > 0) + { + for (var i = 0; i < _constructors.Count; i++) + { + if (writeLine || i > 0) + { + writer.WriteLine(); + } + + _constructors[i] + .SetTypeName(_name!) + .Build(writer); + } + + writeLine = true; + } + + if (Properties.Count > 0) + { + for (var i = 0; i < Properties.Count; i++) + { + if (writeLine || i > 0) + { + writer.WriteLine(); + } + + Properties[i].Build(writer); + } + + writeLine = true; + } + + if (_methods.Count > 0) + { + for (var i = 0; i < _methods.Count; i++) + { + if (writeLine || i > 0) + { + writer.WriteLine(); + } + + _methods[i].Build(writer); + } + } + } + + foreach (var classBuilder in _classes) + { + classBuilder.Build(writer); + } + + writer.WriteIndentedLine("}"); + } +} diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/Builders/CodeBlockBuilder.cs b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/CodeBlockBuilder.cs new file mode 100644 index 0000000..c703eda --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/CodeBlockBuilder.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace StrawberryShake.CodeGeneration.CSharp.Builders; + +public class CodeBlockBuilder : ICode +{ + private readonly List _blockParts = []; + + public static CodeBlockBuilder New() => new CodeBlockBuilder(); + + public static CodeBlockBuilder From(StringBuilder sourceText) + { + var builder = New(); + + using var stringReader = new StringReader(sourceText.ToString()); + + string? line = null; + + do + { + line = stringReader.ReadLine(); + + if (line is not null) + { + builder.AddCode(CodeLineBuilder.From(line)); + } + } while (line is not null); + + return builder; + } + + public CodeBlockBuilder AddCode(ICodeBuilder value) + { + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + + _blockParts.Add(value); + return this; + } + + public CodeBlockBuilder AddCode(string value) + { + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + + _blockParts.Add(CodeInlineBuilder.New().SetText(value)); + return this; + } + + public CodeBlockBuilder AddEmptyLine() + { + _blockParts.Add(CodeLineBuilder.New()); + return this; + } + + public void Build(CodeWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + foreach (var code in _blockParts) + { + code.Build(writer); + } + } +} diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/Builders/CodeFileBuilder.cs b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/CodeFileBuilder.cs new file mode 100644 index 0000000..6edff13 --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/CodeFileBuilder.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; + +namespace StrawberryShake.CodeGeneration.CSharp.Builders; + +public class CodeFileBuilder : ICodeBuilder +{ + private readonly List _usings = []; + private string? _namespace; + private readonly List _types = []; + + public static CodeFileBuilder New() => new(); + + public CodeFileBuilder AddUsing(string value) + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentException(nameof(value)); + } + + _usings.Add(value); + return this; + } + + public CodeFileBuilder SetNamespace(string value) + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentException(nameof(value)); + } + + _namespace = value; + return this; + } + + public CodeFileBuilder AddType(ITypeBuilder value) + { + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + + _types.Add(value); + return this; + } + + public void Build(CodeWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (_types.Count == 0 && _usings.Count == 0) + { + return; + } + + if (_namespace is null) + { + throw new CodeGeneratorException("Namespace cannot be null."); + } + + BuildInternal(writer); + } + + private void BuildInternal(CodeWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (_types.Count == 0 && _usings.Count == 0) + { + return; + } + + if (_namespace is null) + { + throw new CodeGeneratorException("Namespace cannot be null."); + } + + if (_usings.Count > 0) + { + foreach (var u in _usings) + { + writer.WriteIndentedLine($"using {u};"); + } + writer.WriteLine(); + } + + writer.WriteIndentedLine("#nullable enable"); + writer.WriteLine(); + + writer.WriteIndentedLine($"namespace {_namespace}"); + writer.WriteIndentedLine("{"); + + using (writer.IncreaseIndent()) + { + foreach (var type in _types) + { + type.Build(writer); + } + } + + writer.WriteIndentedLine("}"); + } +} diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/Builders/CodeInlineBlockBuilder.cs b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/CodeInlineBlockBuilder.cs new file mode 100644 index 0000000..7159b46 --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/CodeInlineBlockBuilder.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; + +namespace StrawberryShake.CodeGeneration.CSharp.Builders; + +public class CodeInlineBlockBuilder : ICode +{ + private readonly List _lineParts = []; + + public static CodeInlineBlockBuilder New() => new(); + + public CodeInlineBlockBuilder AddCode(ICode value) + { + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + + _lineParts.Add(value); + return this; + } + + public void Build(CodeWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + foreach (var code in _lineParts) + { + code.Build(writer); + } + } +} diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/Builders/CodeInlineBuilder.cs b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/CodeInlineBuilder.cs new file mode 100644 index 0000000..53dc833 --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/CodeInlineBuilder.cs @@ -0,0 +1,34 @@ +using System; + +namespace StrawberryShake.CodeGeneration.CSharp.Builders; + +public class CodeInlineBuilder : ICode +{ + private string? _value; + + public static CodeInlineBuilder New() => new(); + + public static CodeInlineBuilder From(string sourceText) => + New().SetText(sourceText); + + public CodeInlineBuilder SetText(string value) + { + _value = value; + return this; + } + + public void Build(CodeWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (_value is null) + { + return; + } + + writer.Write(_value); + } +} diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/Builders/CodeLineBuilder.cs b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/CodeLineBuilder.cs new file mode 100644 index 0000000..559a3a6 --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/CodeLineBuilder.cs @@ -0,0 +1,58 @@ +using System; + +namespace StrawberryShake.CodeGeneration.CSharp.Builders; + +public class CodeLineBuilder : ICode +{ + private bool _writeLine = true; + private ICode? _value; + private string? _sourceText; + + public static CodeLineBuilder New() => new CodeLineBuilder(); + + public static CodeLineBuilder From(string line) => New().SetLine(line); + + public CodeLineBuilder SetLine(string value) + { + _sourceText = value; + _value = null; + return this; + } + + public CodeLineBuilder SetLine(ICode value) + { + _value = value; + _sourceText = null; + return this; + } + + public CodeLineBuilder SetWriteLine(bool writeLine) + { + _writeLine = writeLine; + return this; + } + + public void Build(CodeWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (_value is not null) + { + writer.WriteIndent(); + _value.Build(writer); + } + else if (_sourceText is not null) + { + writer.WriteIndent(); + writer.Write(_sourceText); + } + + if (_writeLine) + { + writer.WriteLine(); + } + } +} diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/Builders/ConditionBuilder.cs b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/ConditionBuilder.cs new file mode 100644 index 0000000..c4c920f --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/ConditionBuilder.cs @@ -0,0 +1,115 @@ +using System.Collections.Generic; + +namespace StrawberryShake.CodeGeneration.CSharp.Builders; + +public class ConditionBuilder : ICode +{ + private readonly List _conditions = []; + private bool _setReturn; + private bool _determineStatement; + public static ConditionBuilder New() => new(); + + public ConditionBuilder Set(string condition) + { + _conditions.Add(CodeInlineBuilder.New().SetText(condition)); + return this; + } + + public ConditionBuilder SetReturn(bool value = true) + { + _setReturn = value; + return this; + } + + public ConditionBuilder Set(ICode condition) + { + _conditions.Add(condition); + return this; + } + + public ConditionBuilder SetDetermineStatement(bool value = true) + { + _determineStatement = value; + return this; + } + + public ConditionBuilder And(string condition, bool applyIf = true) + { + return applyIf ? And(CodeInlineBuilder.New().SetText(condition)) : this; + } + + public ConditionBuilder And(ICode condition) + { + if (_conditions.Count == 0) + { + return Set(condition); + } + + _conditions.Add( + CodeBlockBuilder.New() + .AddCode(CodeInlineBuilder.New().SetText("&& ")) + .AddCode(condition)); + return this; + } + + public ConditionBuilder Or(ICode condition) + { + if (_conditions.Count == 0) + { + return Set(condition); + } + + _conditions.Add( + CodeBlockBuilder.New() + .AddCode(CodeInlineBuilder.New().SetText("|| ")) + .AddCode(condition)); + return this; + } + + public void Build(CodeWriter writer) + { + if (_determineStatement) + { + writer.WriteIndent(); + } + + if (_setReturn) + { + writer.Write("return "); + } + + if (_conditions.Count != 0) + { + using (writer.IncreaseIndent()) + { + WriteCondition(writer, _conditions[0]); + for (var i = 1; i < _conditions.Count; i++) + { + CodeLineBuilder.New().Build(writer); + writer.WriteIndent(); + WriteCondition(writer, _conditions[i]); + } + } + } + + if (_determineStatement) + { + writer.Write(";"); + writer.WriteLine(); + } + } + + private void WriteCondition(CodeWriter writer, ICode condition) + { + if (condition is ConditionBuilder) + { + writer.Write("("); + condition.Build(writer); + writer.Write(")"); + } + else + { + condition.Build(writer); + } + } +} diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/Builders/ConstructorBuilder.cs b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/ConstructorBuilder.cs new file mode 100644 index 0000000..69db927 --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/ConstructorBuilder.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace StrawberryShake.CodeGeneration.CSharp.Builders; + +public class ConstructorBuilder : ICodeBuilder +{ + private AccessModifier _accessModifier = AccessModifier.Public; + private string? _typeName; + private readonly List _parameters = []; + private readonly List _lines = []; + private readonly List _base = []; + + public static ConstructorBuilder New() => new ConstructorBuilder(); + + public ConstructorBuilder SetAccessModifier(AccessModifier value) + { + _accessModifier = value; + return this; + } + + public ConstructorBuilder SetTypeName(string value) + { + _typeName = value; + return this; + } + + public ConstructorBuilder AddParameter(ParameterBuilder value) + { + _parameters.Add(value); + return this; + } + + public bool HasParameters() + { + return _parameters.Any(); + } + + public ConstructorBuilder AddCode(ICode value) + { + _lines.Add(value); + return this; + } + + public ConstructorBuilder AddCode(string value) + { + _lines.Add(CodeLineBuilder.New().SetLine(value)); + return this; + } + + public ConstructorBuilder AddBase(ICode value) + { + _base.Add(value); + return this; + } + + public ConstructorBuilder AddBase(string value) + { + _base.Add(CodeInlineBuilder.New().SetText(value)); + return this; + } + + public void Build(CodeWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + var modifier = _accessModifier.ToString().ToLowerInvariant(); + + writer.WriteIndent(); + + writer.Write($"{modifier} {_typeName}("); + + if (_parameters.Count == 0) + { + writer.Write(")"); + } + else if (_parameters.Count == 1) + { + _parameters[0].Build(writer); + writer.Write(")"); + } + else + { + writer.WriteLine(); + + using (writer.IncreaseIndent()) + { + for (var i = 0; i < _parameters.Count; i++) + { + writer.WriteIndent(); + _parameters[i].Build(writer); + if (i == _parameters.Count - 1) + { + writer.Write(")"); + } + else + { + writer.Write(","); + writer.WriteLine(); + } + } + } + } + + if (_base.Count > 0) + { + writer.WriteLine(); + + using (writer.IncreaseIndent()) + { + writer.WriteIndent(); + writer.Write(": base("); + if (_base.Count == 1) + { + _base[0].Build(writer); + writer.Write(")"); + } + else + { + using (writer.IncreaseIndent()) + { + for (var i = 0; i < _base.Count; i++) + { + writer.WriteLine(); + writer.WriteIndent(); + _base[i].Build(writer); + if (i == _base.Count - 1) + { + writer.Write(")"); + } + else + { + writer.Write(","); + } + } + } + } + } + } + + writer.WriteLine(); + writer.WriteIndentedLine("{"); + + using (writer.IncreaseIndent()) + { + foreach (var code in _lines) + { + code.Build(writer); + } + } + + writer.WriteIndentedLine("}"); + } +} diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/Builders/EnumBuilder.cs b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/EnumBuilder.cs new file mode 100644 index 0000000..bbebf5c --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/EnumBuilder.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; + +namespace StrawberryShake.CodeGeneration.CSharp.Builders; + +public class EnumBuilder : ITypeBuilder +{ + private AccessModifier _accessModifier; + private readonly List<(string, long?, XmlCommentBuilder)> _elements = []; + private string? _name; + private string? _underlyingType; + private XmlCommentBuilder? _xmlComment; + + public static EnumBuilder New() => new(); + + public EnumBuilder SetAccessModifier(AccessModifier value) + { + _accessModifier = value; + return this; + } + + public EnumBuilder SetName(string value) + { + _name = value; + return this; + } + + public EnumBuilder SetUnderlyingType(RuntimeTypeInfo? value) + { + _underlyingType = value?.ToString(); + return this; + } + + public EnumBuilder SetUnderlyingType(string? value) + { + _underlyingType = value; + return this; + } + + public EnumBuilder AddElement(string name, long? value = null, string? documentation = null) + { + _elements.Add(( + name, + value, + documentation is null + ? null + : XmlCommentBuilder.New().SetSummary(documentation))); + return this; + } + + public EnumBuilder SetComment(string? xmlComment) + { + if (xmlComment is not null) + { + _xmlComment = XmlCommentBuilder.New().SetSummary(xmlComment); + } + + return this; + } + + public void Build(CodeWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + _xmlComment?.Build(writer); + + writer.WriteGeneratedAttribute(); + + var modifier = _accessModifier.ToString().ToLowerInvariant(); + + if (_underlyingType is null) + { + writer.WriteIndentedLine($"{modifier} enum {_name}"); + } + else + { + writer.WriteIndentedLine($"{modifier} enum {_name} : {_underlyingType}"); + } + + writer.WriteIndentedLine("{"); + + using (writer.IncreaseIndent()) + { + for (var i = 0; i < _elements.Count; i++) + { + _elements[i].Item3?.Build(writer); + + writer.WriteIndent(); + writer.Write(_elements[i].Item1); + + if (_elements[i].Item2.HasValue) + { + writer.Write($" = {_elements[i].Item2}"); + } + + if (i + 1 < _elements.Count) + { + writer.Write($","); + } + + writer.WriteLine(); + } + } + + writer.WriteIndentedLine("}"); + } +} diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/Builders/ExceptionBuilder.cs b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/ExceptionBuilder.cs new file mode 100644 index 0000000..bd4c1b1 --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/ExceptionBuilder.cs @@ -0,0 +1,52 @@ +namespace StrawberryShake.CodeGeneration.CSharp.Builders; + +public class ExceptionBuilder : ICode +{ + private readonly MethodCallBuilder _method = + MethodCallBuilder + .New() + .SetPrefix("throw new "); + + public ExceptionBuilder SetException(string exception) + { + _method.SetMethodName(exception); + return this; + } + + public ExceptionBuilder AddArgument(string value) + { + _method.AddArgument(value); + return this; + } + + public ExceptionBuilder AddArgument(ICode value) + { + _method.AddArgument(value); + return this; + } + + public ExceptionBuilder SetDetermineStatement(bool value) + { + _method.SetDetermineStatement(value); + return this; + } + + public ExceptionBuilder SetWrapArguments(bool value = true) + { + _method.SetWrapArguments(value); + return this; + } + + public void Build(CodeWriter writer) + { + _method.Build(writer); + } + + public static ExceptionBuilder New() => new(); + + public static ExceptionBuilder New(string types) => + new ExceptionBuilder().SetException(types); + + public static ExceptionBuilder Inline(string types) => + new ExceptionBuilder().SetException(types).SetDetermineStatement(false); +} diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/Builders/FieldBuilder.cs b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/FieldBuilder.cs new file mode 100644 index 0000000..36bade5 --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/FieldBuilder.cs @@ -0,0 +1,147 @@ +using System; + +namespace StrawberryShake.CodeGeneration.CSharp.Builders; + +public class FieldBuilder : ICodeBuilder +{ + private AccessModifier _accessModifier = AccessModifier.Private; + private bool _isConst; + private bool _isStatic; + private bool _isReadOnly; + private TypeReferenceBuilder? _type; + private string? _name; + private ICode? _value; + private bool _useDefaultInitializer; + private bool _beginValueWithNewline; + + public static FieldBuilder New() => new FieldBuilder(); + + public FieldBuilder SetAccessModifier(AccessModifier value) + { + _accessModifier = value; + return this; + } + + public FieldBuilder SetType(string value, bool condition = true) + { + if (condition) + { + _type = TypeReferenceBuilder.New().SetName(value); + } + + return this; + } + + public FieldBuilder SetType(TypeReferenceBuilder typeReference) + { + _type = typeReference; + return this; + } + + public FieldBuilder SetName(string value) + { + _name = value; + return this; + } + + public FieldBuilder SetConst() + { + _isConst = true; + _isStatic = false; + _isReadOnly = false; + return this; + } + + public FieldBuilder SetStatic() + { + _isStatic = true; + _isConst = false; + return this; + } + + public FieldBuilder SetReadOnly() + { + _isReadOnly = true; + _isConst = false; + return this; + } + + public FieldBuilder SetValue(string? value, bool beginValueWithNewline = false) + { + return SetValue( + value is not null ? CodeInlineBuilder.From(value) : null, + beginValueWithNewline); + } + + public FieldBuilder SetValue(ICode? value, bool beginValueWithNewline = false) + { + _value = value; + _beginValueWithNewline = beginValueWithNewline; + _useDefaultInitializer = false; + return this; + } + + public FieldBuilder UseDefaultInitializer() + { + _value = null; + _useDefaultInitializer = true; + return this; + } + + public void Build(CodeWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (_type is null) + { + throw new ArgumentNullException(nameof(_type)); + } + + var modifier = _accessModifier.ToString().ToLowerInvariant(); + + writer.WriteIndent(); + writer.Write($"{modifier} "); + + if (_isConst) + { + writer.Write("const "); + } + + if (_isStatic) + { + writer.Write("static "); + } + + if (_isReadOnly) + { + writer.Write("readonly "); + } + + _type.Build(writer); + writer.Write(_name); + + if (_value is { }) + { + writer.Write(" = "); + if (_beginValueWithNewline) + { + writer.WriteLine(); + using (writer.IncreaseIndent()) + { + writer.WriteIndent(); + } + } + + _value.Build(writer); + } + else if (_useDefaultInitializer) + { + writer.Write($" = new {_type}()"); + } + + writer.WriteLine(";"); + } +} diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/Builders/ForEachBuilder.cs b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/ForEachBuilder.cs new file mode 100644 index 0000000..c52f421 --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/ForEachBuilder.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; + +namespace StrawberryShake.CodeGeneration.CSharp.Builders; + +public class ForEachBuilder : ICodeContainer +{ + private ICode? _loopHeader; + private readonly List _lines = []; + + public static ForEachBuilder New() => new(); + + public ForEachBuilder AddCode(string code, bool addIf = true) + { + AddCode( + CodeLineBuilder.New().SetLine(code), + addIf); + return this; + } + + public ForEachBuilder AddCode(ICode code, bool addIf = true) + { + if (addIf) + { + _lines.Add(code); + } + + return this; + } + + public ForEachBuilder AddEmptyLine() + { + _lines.Add(CodeLineBuilder.New()); + return this; + } + + public ForEachBuilder SetLoopHeader(string elementCode) + { + _loopHeader = CodeInlineBuilder.New().SetText(elementCode); + return this; + } + + public ForEachBuilder SetLoopHeader(ICode elementCode) + { + _loopHeader = elementCode; + return this; + } + + public void Build(CodeWriter writer) + { + writer.WriteIndent(); + writer.Write("foreach ("); + _loopHeader?.Build(writer); + writer.Write(")"); + writer.WriteLine(); + writer.WriteIndent(); + writer.WriteLine("{"); + using (writer.IncreaseIndent()) + { + foreach (var line in _lines) + { + line.Build(writer); + } + } + + writer.WriteIndent(); + writer.WriteLine("}"); + } +} diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/Builders/HashCodeBuilder.cs b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/HashCodeBuilder.cs new file mode 100644 index 0000000..d471243 --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/HashCodeBuilder.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using StrawberryShake.CodeGeneration.CSharp.Builders; + +namespace StrawberryShake.CodeGeneration.CSharp.Generators; + +internal class HashCodeBuilder : ICode +{ + public const string VariableName = "hash"; + public const int Prime = 397; + + private readonly List _code = []; + + public static HashCodeBuilder New() => new(); + + public HashCodeBuilder AddCode(ICode code) + { + _code.Add(code); + return this; + } + + public void Build(CodeWriter writer) + { + writer.WriteIndentedLine("unchecked"); + writer.WriteIndentedLine("{"); + + using (writer.IncreaseIndent()) + { + writer.WriteIndentedLine($"int {VariableName} = 5;"); + writer.WriteLine(); + foreach (var check in _code) + { + check.Build(writer); + writer.WriteLine(); + } + + writer.WriteIndentedLine($"return {VariableName};"); + } + + writer.WriteIndentedLine("}"); + } +} diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/Builders/ICode.cs b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/ICode.cs new file mode 100644 index 0000000..4348e5f --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/ICode.cs @@ -0,0 +1,3 @@ +namespace StrawberryShake.CodeGeneration.CSharp.Builders; + +public interface ICode : ICodeBuilder; diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/Builders/ICodeBuilder.cs b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/ICodeBuilder.cs new file mode 100644 index 0000000..397bbc8 --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/ICodeBuilder.cs @@ -0,0 +1,6 @@ +namespace StrawberryShake.CodeGeneration.CSharp.Builders; + +public interface ICodeBuilder +{ + void Build(CodeWriter writer); +} diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/Builders/ICodeContainer.cs b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/ICodeContainer.cs new file mode 100644 index 0000000..aa37501 --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/ICodeContainer.cs @@ -0,0 +1,10 @@ +namespace StrawberryShake.CodeGeneration.CSharp.Builders; + +public interface ICodeContainer: ICode +{ + public T AddCode(string code, bool addIf = true); + + public T AddCode(ICode code, bool addIf = true); + + public T AddEmptyLine(); +} diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/Builders/ITypeBuilder.cs b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/ITypeBuilder.cs new file mode 100644 index 0000000..8063a79 --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/ITypeBuilder.cs @@ -0,0 +1,3 @@ +namespace StrawberryShake.CodeGeneration.CSharp.Builders; + +public interface ITypeBuilder : ICodeBuilder; diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/Builders/IfBuilder.cs b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/IfBuilder.cs new file mode 100644 index 0000000..04c9d64 --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/IfBuilder.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; + +namespace StrawberryShake.CodeGeneration.CSharp.Builders; + +public class IfBuilder : ICodeContainer +{ + private readonly List _lines = []; + private ConditionBuilder? _condition; + + private readonly List _ifElses = []; + private ICode? _elseCode; + private bool _writeIndents = true; + + public static IfBuilder New() => new(); + + public IfBuilder SetCondition(ConditionBuilder condition) + { + _condition = condition; + return this; + } + + public IfBuilder SkipIndents() + { + _writeIndents = false; + return this; + } + + public IfBuilder SetCondition(string condition) + { + _condition = ConditionBuilder.New().Set(condition); + return this; + } + + public IfBuilder SetCondition(ICode condition) + { + _condition = ConditionBuilder.New().Set(condition); + return this; + } + + public IfBuilder AddCode(string code, bool addIf = true) + { + if (addIf) + { + _lines.Add(CodeLineBuilder.New().SetLine(code)); + } + + return this; + } + + public IfBuilder AddCode(ICode code, bool addIf = true) + { + if (addIf) + { + _lines.Add(code); + } + + return this; + } + + public IfBuilder AddEmptyLine() + { + _lines.Add(CodeLineBuilder.New()); + return this; + } + + public IfBuilder AddIfElse(IfBuilder singleIf) + { + _ifElses.Add(singleIf); + return this; + } + + public IfBuilder AddElse(ICode code) + { + _elseCode = code; + return this; + } + + public void Build(CodeWriter writer) + { + if (_condition is null) + { + throw new ArgumentNullException(nameof(_condition)); + } + + if (_writeIndents) + { + writer.WriteIndent(); + } + + writer.Write("if ("); + _condition.Build(writer); + writer.Write(")"); + writer.WriteLine(); + writer.WriteIndentedLine("{"); + + using (writer.IncreaseIndent()) + { + foreach (var code in _lines) + { + code.Build(writer); + } + } + + writer.WriteIndent(); + writer.Write("}"); + writer.WriteLine(); + + foreach (var ifBuilder in _ifElses) + { + writer.WriteIndent(); + writer.Write("else "); + ifBuilder.Build(writer); + } + + if (_elseCode is not null) + { + writer.WriteIndentedLine("else"); + writer.WriteIndentedLine("{"); + using (writer.IncreaseIndent()) + { + _elseCode.Build(writer); + } + + writer.WriteIndentedLine("}"); + } + } +} diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/Builders/Inheritance.cs b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/Inheritance.cs new file mode 100644 index 0000000..7501b20 --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/Inheritance.cs @@ -0,0 +1,9 @@ +namespace StrawberryShake.CodeGeneration.CSharp.Builders; + +public enum Inheritance +{ + None, + Sealed, + Override, + Virtual, +} diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/Builders/InterfaceBuilder.cs b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/InterfaceBuilder.cs new file mode 100644 index 0000000..3bcc90b --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/InterfaceBuilder.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; + +namespace StrawberryShake.CodeGeneration.CSharp.Builders; + +public class InterfaceBuilder : AbstractTypeBuilder +{ + private AccessModifier _accessModifier; + private readonly List _methods = []; + + private XmlCommentBuilder? _xmlComment; + + public static InterfaceBuilder New() => new(); + + public InterfaceBuilder SetAccessModifier(AccessModifier value) + { + _accessModifier = value; + return this; + } + + public new InterfaceBuilder SetName(string name) + { + base.SetName(name); + return this; + } + + public new InterfaceBuilder AddImplements(string value) + { + base.AddImplements(value); + return this; + } + + public new InterfaceBuilder AddProperty(PropertyBuilder property) + { + base.AddProperty(property); + return this; + } + + public InterfaceBuilder SetComment(string? xmlComment) + { + if (xmlComment is not null) + { + _xmlComment = XmlCommentBuilder.New().SetSummary(xmlComment); + } + + return this; + } + + public InterfaceBuilder SetComment(XmlCommentBuilder? xmlComment) + { + if (xmlComment is not null) + { + _xmlComment = xmlComment; + } + + return this; + } + + public InterfaceBuilder AddMethod(MethodBuilder method) + { + if (method is null) + { + throw new ArgumentNullException(nameof(method)); + } + + _methods.Add(method); + return this; + } + + public override void Build(CodeWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + _xmlComment?.Build(writer); + + writer.WriteGeneratedAttribute(); + + writer.WriteIndent(); + + var modifier = _accessModifier.ToString().ToLowerInvariant(); + + writer.Write($"{modifier} partial interface "); + writer.WriteLine(Name); + + if (Implements.Count > 0) + { + using (writer.IncreaseIndent()) + { + for (var i = 0; i < Implements.Count; i++) + { + writer.WriteIndentedLine( + i == 0 + ? $": {Implements[i]}" + : $", {Implements[i]}"); + } + } + } + + writer.WriteIndentedLine("{"); + + var writeLine = false; + + using (writer.IncreaseIndent()) + { + if (Properties.Count > 0) + { + for (var i = 0; i < Properties.Count; i++) + { + if (writeLine || i > 0) + { + writer.WriteLine(); + } + + Properties[i].Build(writer); + } + + writeLine = true; + } + + if (_methods.Count > 0) + { + for (var i = 0; i < _methods.Count; i++) + { + if (writeLine || i > 0) + { + writer.WriteLine(); + } + + _methods[i].Build(writer); + } + } + } + + writer.WriteIndentedLine("}"); + } +} diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/Builders/LambdaBuilder.cs b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/LambdaBuilder.cs new file mode 100644 index 0000000..d2315ac --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/LambdaBuilder.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; + +namespace StrawberryShake.CodeGeneration.CSharp.Builders; + +public class LambdaBuilder : ICode +{ + private bool _block; + private bool _isAsync; + private readonly List _arguments = []; + private ICode? _code; + + public LambdaBuilder AddArgument(string value) + { + _arguments.Add(value); + return this; + } + + public LambdaBuilder SetCode(ICode code) + { + _code = code; + return this; + } + + public LambdaBuilder SetBlock(bool block) + { + _block = block; + return this; + } + + public LambdaBuilder SetAsync(bool value = true) + { + _isAsync = value; + return this; + } + + public void Build(CodeWriter writer) + { + if (_code is null) + { + throw new ArgumentNullException(nameof(_code)); + } + + if (_isAsync) + { + writer.Write("async "); + } + + if (_arguments.Count > 1) + { + writer.Write('('); + } + + for (var i = 0; i < _arguments.Count; i++) + { + if (i > 0) + { + writer.Write(','); + } + + writer.Write(_arguments[i]); + } + + if (_arguments.Count > 1) + { + writer.Write(')'); + } + + if (_arguments.Count == 0) + { + writer.Write("()"); + } + + writer.Write(" => "); + + if (_block) + { + writer.WriteLine(); + writer.WriteIndent(); + writer.WriteLeftBrace(); + writer.WriteLine(); + writer.IncreaseIndent(); + } + + _code.Build(writer); + + if (_block) + { + writer.DecreaseIndent(); + writer.WriteIndent(); + writer.WriteRightBrace(); + } + } + + public static LambdaBuilder New() => new(); +} diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/Builders/MethodBuilder.cs b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/MethodBuilder.cs new file mode 100644 index 0000000..245c55d --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/MethodBuilder.cs @@ -0,0 +1,245 @@ +using System; +using System.Collections.Generic; + +namespace StrawberryShake.CodeGeneration.CSharp.Builders; + +public class MethodBuilder : ICodeContainer +{ + private AccessModifier _accessModifier = AccessModifier.Private; + private Inheritance _inheritance = Inheritance.None; + private bool _isStatic; + private bool _isOnlyDeclaration; + private bool _isOverride; + private bool _is; + private TypeReferenceBuilder _returnType = TypeReferenceBuilder.New().SetName("void"); + private string? _name; + private readonly List _parameters = []; + private readonly List _lines = []; + private bool _isAsync; + private string? _interface; + + public static MethodBuilder New() => new(); + + public MethodBuilder SetAccessModifier(AccessModifier value) + { + _accessModifier = value; + return this; + } + + public MethodBuilder SetStatic() + { + _isStatic = true; + return this; + } + + public MethodBuilder SetAsync() + { + _isAsync = true; + return this; + } + + public MethodBuilder SetInterface(string value) + { + _interface = value; + return this; + } + + public MethodBuilder SetOverride() + { + _isOverride = true; + return this; + } + + public MethodBuilder Set() + { + _is = true; + return this; + } + + public MethodBuilder SetInheritance(Inheritance value) + { + _inheritance = value; + return this; + } + + public MethodBuilder SetOnlyDeclaration(bool value = true) + { + _isOnlyDeclaration = value; + return this; + } + + public MethodBuilder SetReturnType(string value, bool condition = true) + { + if (condition) + { + _returnType = TypeReferenceBuilder.New().SetName(value); + } + + return this; + } + + public MethodBuilder SetReturnType(TypeReferenceBuilder value, bool condition = true) + { + if (condition) + { + _returnType = value; + } + + return this; + } + + public MethodBuilder SetName(string value) + { + _name = value; + return this; + } + + public MethodBuilder AddParameter(ParameterBuilder value) + { + _parameters.Add(value); + return this; + } + + public MethodBuilder AddCode(string code, bool addIf = true) + { + if (addIf) + { + _lines.Add(CodeLineBuilder.New().SetLine(code)); + } + + return this; + } + + public MethodBuilder AddCode(ICode code, bool addIf = true) + { + if (addIf) + { + _lines.Add(code); + } + + return this; + } + + public MethodBuilder AddEmptyLine() + { + _lines.Add(CodeLineBuilder.New()); + return this; + } + + public MethodBuilder AddInlineCode(string code) + { + _lines.Add(CodeInlineBuilder.New().SetText(code)); + return this; + } + + public void Build(CodeWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + var modifier = _accessModifier.ToString().ToLowerInvariant(); + + writer.WriteIndent(); + + if (_interface is null && !_isOnlyDeclaration) + { + writer.Write($"{modifier} "); + + if (_isStatic) + { + writer.Write("static "); + } + + if (_isOverride) + { + writer.Write("override "); + } + + if (_isAsync) + { + writer.Write("async "); + } + + if (_is) + { + writer.Write(" "); + } + + writer.Write($"{CreateInheritance()}"); + } + + _returnType.Build(writer); + + if (_interface is not null) + { + writer.Write(_interface); + writer.Write("."); + } + + writer.Write($"{_name}("); + + if (_parameters.Count == 0) + { + writer.Write(")"); + } + else if (_parameters.Count == 1) + { + _parameters[0].Build(writer); + writer.Write(")"); + } + else + { + writer.WriteLine(); + + using (writer.IncreaseIndent()) + { + for (var i = 0; i < _parameters.Count; i++) + { + writer.WriteIndent(); + _parameters[i].Build(writer); + if (i == _parameters.Count - 1) + { + writer.Write(")"); + } + else + { + writer.Write(","); + writer.WriteLine(); + } + } + } + } + + if (_isOnlyDeclaration) + { + writer.Write(";"); + writer.WriteLine(); + } + else + { + writer.WriteLine(); + writer.WriteIndentedLine("{"); + + using (writer.IncreaseIndent()) + { + foreach (var code in _lines) + { + code.Build(writer); + } + } + + writer.WriteIndentedLine("}"); + } + } + + private string CreateInheritance() + => _inheritance switch + { + Inheritance.Override => "override ", + Inheritance.Sealed => "sealed override ", + Inheritance.Virtual => "virtual ", + _ => string.Empty, + }; +} diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/Builders/MethodCallBuilder.cs b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/MethodCallBuilder.cs new file mode 100644 index 0000000..f0d7228 --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/MethodCallBuilder.cs @@ -0,0 +1,275 @@ +using System; +using System.Collections.Generic; + +namespace StrawberryShake.CodeGeneration.CSharp.Builders; + +public class MethodCallBuilder : ICode +{ + private string[] _methodName = []; + private bool _determineStatement = true; + private bool _setNullForgiving; + private bool _wrapArguments; + private bool _setReturn; + private bool _setNew; + private bool _setAwait; + private string? _prefix; + private readonly List _arguments = []; + private readonly List _generics = []; + private readonly List _chainedCode = []; + + public static MethodCallBuilder New() => new(); + + public static MethodCallBuilder Inline() => New().SetDetermineStatement(false); + + public MethodCallBuilder SetPrefix(string prefix) + { + _prefix = prefix; + return this; + } + + public MethodCallBuilder SetMethodName(string methodName) + { + _methodName = + [ + methodName, + ]; + return this; + } + + public MethodCallBuilder SetMethodName(params string[] methodName) + { + _methodName = methodName; + return this; + } + + public MethodCallBuilder AddChainedCode(ICode value) + { + _chainedCode.Add(value); + return this; + } + + public MethodCallBuilder AddArgument(ICode value) + { + _arguments.Add(value); + return this; + } + + public MethodCallBuilder AddGeneric(ICode value) + { + _generics.Add(value); + return this; + } + + public MethodCallBuilder AddGeneric(string value) + { + _generics.Add(CodeInlineBuilder.New().SetText(value)); + return this; + } + + public MethodCallBuilder AddArgument(string value) + { + _arguments.Add(CodeInlineBuilder.New().SetText(value)); + return this; + } + + public MethodCallBuilder AddOutArgument( + string value, + string typeReference) + { + _arguments.Add(CodeInlineBuilder.New().SetText($"out {typeReference}? {value}")); + return this; + } + + public MethodCallBuilder SetDetermineStatement(bool value) + { + _determineStatement = value; + return this; + } + + public MethodCallBuilder SetWrapArguments(bool value = true) + { + _wrapArguments = value; + return this; + } + + public MethodCallBuilder SetNullForgiving(bool value = true) + { + _setNullForgiving = value; + return this; + } + + public MethodCallBuilder SetReturn(bool value = true) + { + _setReturn = value; + return this; + } + + public MethodCallBuilder SetNew(bool value = true) + { + _setNew = value; + return this; + } + + public MethodCallBuilder SetAwait(bool value = true) + { + _setAwait = value; + return this; + } + + public void Build(CodeWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (_determineStatement) + { + writer.WriteIndent(); + } + + if (_setReturn) + { + writer.Write("return "); + } + + if (_setNew) + { + writer.Write("new "); + } + + if (_setAwait) + { + writer.Write("await "); + } + + writer.Write(_prefix); + + if (_methodName.Length > 0) + { + for (var i = 0; i < _methodName.Length - 1; i++) + { + writer.Write(_methodName[i]); + if (i < _methodName.Length - 2) + { + writer.Write("."); + } + } + + if (_chainedCode.Count > 0) + { + writer.WriteLine(); + writer.IncreaseIndent(); + writer.WriteIndent(); + } + + if (_methodName.Length > 1) + { + writer.Write("."); + } + + writer.Write(_methodName[_methodName.Length - 1]); + + if (_generics.Count > 0) + { + writer.Write("<"); + for (var i = 0; i < _generics.Count; i++) + { + _generics[i].Build(writer); + if (i == _generics.Count - 1) + { + writer.Write(">"); + } + else + { + writer.Write(", "); + } + } + } + + writer.Write("("); + + if (_arguments.Count == 0) + { + writer.Write(")"); + if (_setNullForgiving) + { + writer.Write("!"); + } + } + else if (_arguments.Count == 1) + { + if (_wrapArguments) + { + writer.WriteLine(); + writer.IncreaseIndent(); + writer.WriteIndent(); + } + + _arguments[0].Build(writer); + if (_wrapArguments) + { + writer.DecreaseIndent(); + writer.Write(")"); + } + else + { + writer.Write(")"); + } + + if (_setNullForgiving) + { + writer.Write("!"); + } + } + else + { + writer.WriteLine(); + + using (writer.IncreaseIndent()) + { + for (var i = 0; i < _arguments.Count; i++) + { + writer.WriteIndent(); + _arguments[i].Build(writer); + if (i == _arguments.Count - 1) + { + writer.Write(")"); + if (_setNullForgiving) + { + writer.Write("!"); + } + } + else + { + writer.Write(","); + writer.WriteLine(); + } + } + } + } + + if (_chainedCode.Count > 0) + { + writer.DecreaseIndent(); + } + } + + using (writer.IncreaseIndent()) + { + foreach (var code in _chainedCode) + { + writer.WriteLine(); + writer.WriteIndent(); + writer.Write('.'); + code.Build(writer); + } + } + + if (_determineStatement) + { + writer.Write(";"); + writer.WriteLine(); + } + } +} diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/Builders/NullCheckBuilder.cs b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/NullCheckBuilder.cs new file mode 100644 index 0000000..379626d --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/NullCheckBuilder.cs @@ -0,0 +1,94 @@ +using System; + +namespace StrawberryShake.CodeGeneration.CSharp.Builders; + +public class NullCheckBuilder : ICode +{ + private ICode? _condition; + private ICode? _code; + private bool _determineStatement = true; + private bool _singleLine; + + public NullCheckBuilder SetCondition(ICode condition) + { + _condition = condition; + return this; + } + + public NullCheckBuilder SetCondition(string condition) + { + _condition = CodeInlineBuilder.From(condition); + return this; + } + + public NullCheckBuilder SetCode(ICode code) + { + _code = code; + return this; + } + + public NullCheckBuilder SetCode(string code) + { + _code = CodeInlineBuilder.From(code); + return this; + } + + public NullCheckBuilder SetDetermineStatement(bool value) + { + _determineStatement = value; + return this; + } + + public NullCheckBuilder SetSingleLine(bool value = true) + { + _singleLine = value; + return this; + } + + public void Build(CodeWriter writer) + { + if (_condition is null) + { + throw new ArgumentNullException(nameof(_condition)); + } + + if (_code is null) + { + throw new ArgumentNullException(nameof(_code)); + } + + _condition.Build(writer); + + if (!_singleLine) + { + writer.WriteLine(); + } + + using (writer.IncreaseIndent()) + { + if (!_singleLine) + { + writer.WriteIndent(); + } + else + { + writer.Write(" "); + } + + writer.Write("?? "); + _code.Build(writer); + } + + if (_determineStatement) + { + writer.Write(";"); + writer.WriteLine(); + } + } + + public static NullCheckBuilder New() => new(); + + public static NullCheckBuilder Inline() => New() + .SetDetermineStatement(false) + .SetSingleLine(); +} diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/Builders/ParameterBuilder.cs b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/ParameterBuilder.cs new file mode 100644 index 0000000..71a35ab --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/ParameterBuilder.cs @@ -0,0 +1,76 @@ +using System; + +namespace StrawberryShake.CodeGeneration.CSharp.Builders; + +public class ParameterBuilder : ICodeBuilder +{ + private TypeReferenceBuilder? _type; + private string? _name; + private string? _default; + private bool _this; + + public static ParameterBuilder New() => new(); + + public ParameterBuilder SetType(TypeReferenceBuilder value, bool condition = true) + { + if (condition) + { + _type = value; + } + return this; + } + + public ParameterBuilder SetType(string name) + { + _type = TypeReferenceBuilder.New().SetName(name); + return this; + } + + public ParameterBuilder SetName(string value) + { + _name = value; + return this; + } + + public ParameterBuilder SetThis(bool value = true) + { + _this = value; + return this; + } + + public ParameterBuilder SetDefault(string value = "default", bool condition = true) + { + if (condition) + { + _default = value; + } + return this; + } + + public void Build(CodeWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (_type is null) + { + throw new ArgumentNullException(nameof(_type)); + } + + if (_this) + { + writer.Write("this "); + } + + _type.Build(writer); + + writer.Write(_name); + + if (_default is not null) + { + writer.Write($" = {_default}"); + } + } +} diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/Builders/PropertyBuilder.cs b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/PropertyBuilder.cs new file mode 100644 index 0000000..1fbf897 --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/PropertyBuilder.cs @@ -0,0 +1,174 @@ +using System; + +namespace StrawberryShake.CodeGeneration.CSharp.Builders; + +public class PropertyBuilder : ICodeBuilder +{ + private AccessModifier _accessModifier; + private bool _isReadOnly = true; + private ICode? _lambdaResolver; + private bool _isOnlyDeclaration; + private TypeReferenceBuilder? _type; + private string? _name; + private XmlCommentBuilder? _xmlComment; + private string? _value; + private string? _interface; + private bool _isStatic; + private bool _isOverride; + + public static PropertyBuilder New() => new(); + + public PropertyBuilder SetAccessModifier(AccessModifier value) + { + _accessModifier = value; + return this; + } + + public PropertyBuilder SetStatic() + { + _isStatic = true; + return this; + } + + public PropertyBuilder SetOverride() + { + _isOverride = true; + return this; + } + + public PropertyBuilder AsLambda(string resolveCode) + { + _lambdaResolver = CodeInlineBuilder.From(resolveCode); + return this; + } + + public PropertyBuilder AsLambda(ICode resolveCode) + { + _lambdaResolver = resolveCode; + return this; + } + + public PropertyBuilder SetType(string value) + { + _type = TypeReferenceBuilder.New().SetName(value); + return this; + } + + public PropertyBuilder SetComment(string? xmlComment) + { + if (xmlComment is not null) + { + _xmlComment = XmlCommentBuilder.New().SetSummary(xmlComment); + } + + return this; + } + + public PropertyBuilder SetOnlyDeclaration(bool value = true) + { + _isOnlyDeclaration = value; + return this; + } + + public PropertyBuilder SetType(TypeReferenceBuilder value) + { + _type = value; + return this; + } + + public PropertyBuilder SetName(string value) + { + _name = value; + return this; + } + + public PropertyBuilder SetInterface(string value) + { + _interface = value; + return this; + } + + public PropertyBuilder SetValue(string? value) + { + _value = value; + return this; + } + + public PropertyBuilder MakeSettable() + { + _isReadOnly = false; + return this; + } + + public void Build(CodeWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (_type is null) + { + throw new ArgumentNullException(nameof(_type)); + } + + var modifier = _accessModifier.ToString().ToLowerInvariant(); + + _xmlComment?.Build(writer); + + writer.WriteIndent(); + if (_interface is null && !_isOnlyDeclaration) + { + writer.Write(modifier); + writer.WriteSpace(); + if (_isStatic) + { + writer.Write("static"); + writer.WriteSpace(); + } + + if (_isOverride) + { + writer.Write("override"); + writer.WriteSpace(); + } + } + + _type.Build(writer); + + if (_interface is not null) + { + writer.Write(_interface); + writer.Write("."); + } + + writer.Write(_name); + + if (_lambdaResolver is not null) + { + writer.Write(" => "); + _lambdaResolver.Build(writer); + writer.Write(";"); + writer.WriteLine(); + return; + } + + writer.Write(" {"); + writer.Write(" get;"); + if (!_isReadOnly) + { + writer.Write(" set;"); + } + + writer.Write(" }"); + + if (_value is not null) + { + writer.Write(" = "); + writer.Write(_value); + writer.Write(";"); + } + + writer.WriteLine(); + } +} diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/Builders/SwitchExpressionBuilder.cs b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/SwitchExpressionBuilder.cs new file mode 100644 index 0000000..f2c7ce2 --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/SwitchExpressionBuilder.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; + +namespace StrawberryShake.CodeGeneration.CSharp.Builders; + +public class SwitchExpressionBuilder : ICode +{ + private readonly List<(ICode, ICode)> _cases = []; + private string? _expression; + private bool _determineStatement = true; + private bool _setReturn; + private string? _prefix; + private ICode? _defaultCase; + + public static SwitchExpressionBuilder New() => new(); + + public SwitchExpressionBuilder SetPrefix(string prefix) + { + _prefix = prefix; + return this; + } + + public SwitchExpressionBuilder SetReturn(bool value = true) + { + _setReturn = value; + return this; + } + + public SwitchExpressionBuilder SetExpression(string expression) + { + _expression = expression; + return this; + } + + public SwitchExpressionBuilder AddCase(ICode type, ICode action) + { + _cases.Add((type, action)); + return this; + } + + public SwitchExpressionBuilder AddCase(string type, ICode action) + { + _cases.Add((CodeInlineBuilder.From(type), action)); + return this; + } + + public SwitchExpressionBuilder AddCase(string type, string action) + { + return AddCase(CodeInlineBuilder.From(type), CodeInlineBuilder.From(action)); + } + + public SwitchExpressionBuilder SetDefaultCase(string action) + { + _defaultCase = CodeInlineBuilder.From(action); + return this; + } + + public SwitchExpressionBuilder SetDefaultCase(ICode action) + { + _defaultCase = action; + return this; + } + + public SwitchExpressionBuilder SetDetermineStatement(bool value) + { + _determineStatement = value; + return this; + } + + public void Build(CodeWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (_determineStatement) + { + writer.WriteIndent(); + } + + if (_setReturn) + { + writer.Write("return "); + } + + writer.Write(_prefix); + + writer.Write(_expression); + + writer.Write(" switch"); + writer.WriteLine(); + + writer.WriteIndentedLine("{"); + + using (writer.IncreaseIndent()) + { + for (var i = 0; i < _cases.Count; i++) + { + var (type, action) = _cases[i]; + + writer.WriteIndent(); + type.Build(writer); + writer.Write(" => "); + action.Build(writer); + + if (i < _cases.Count - 1 || _defaultCase is not null) + { + writer.Write(","); + } + + writer.WriteLine(); + } + + if (_defaultCase is not null) + { + writer.WriteIndent(); + writer.Write("_ => "); + _defaultCase.Build(writer); + writer.WriteLine(); + } + } + + writer.WriteIndent(); + writer.Write("}"); + + if (_determineStatement) + { + writer.Write(";"); + writer.WriteLine(); + } + } +} diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/Builders/TryCatchBuilder.cs b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/TryCatchBuilder.cs new file mode 100644 index 0000000..ad1be3c --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/TryCatchBuilder.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; + +namespace StrawberryShake.CodeGeneration.CSharp.Builders; + +public class TryCatchBuilder : ICode +{ + private readonly List _try = []; + private readonly List _catch = []; + + public static TryCatchBuilder New() => new(); + + public TryCatchBuilder AddTryCode(ICode code) + { + _try.Add(code); + return this; + } + + public TryCatchBuilder AddCatchBlock(CatchBlockBuilder code) + { + _catch.Add(code); + return this; + } + + public void Build(CodeWriter writer) + { + if (_catch.Count == 0 || _try.Count == 0) + { + throw new InvalidOperationException( + "The catch build needs at least one try and one catch."); + } + + writer.WriteIndentedLine("try"); + writer.WriteIndentedLine("{"); + + using (writer.IncreaseIndent()) + { + foreach (var code in _try) + { + code.Build(writer); + } + } + + writer.WriteIndentedLine("}"); + + foreach (var code in _catch) + { + code.Build(writer); + } + } +} diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/Builders/TupleBuilder.cs b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/TupleBuilder.cs new file mode 100644 index 0000000..95e05fe --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/TupleBuilder.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; + +namespace StrawberryShake.CodeGeneration.CSharp.Builders; + +public class TupleBuilder : ICode +{ + private bool _determineStatement = false; + private string? _prefix; + private bool _setReturn; + private readonly List _members = []; + + public static TupleBuilder New() => new(); + + public static TupleBuilder Inline() => New().SetDetermineStatement(false); + + public TupleBuilder AddMember(string value) + { + _members.Add(CodeInlineBuilder.New().SetText(value)); + return this; + } + + public TupleBuilder AddMember(ICode value) + { + _members.Add(value); + return this; + } + + public TupleBuilder SetPrefix(string prefix) + { + _prefix = prefix; + return this; + } + + public TupleBuilder SetDetermineStatement(bool value) + { + _determineStatement = value; + return this; + } + + public TupleBuilder SetReturn(bool value = true) + { + _setReturn = value; + return this; + } + + public void Build(CodeWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (_determineStatement) + { + writer.WriteIndent(); + } + + if (_setReturn) + { + writer.Write("return "); + } + + writer.Write(_prefix); + + writer.Write("("); + + if (_members.Count == 0) + { + writer.Write(")"); + } + else if (_members.Count == 1) + { + _members[0].Build(writer); + writer.Write(")"); + } + else + { + writer.WriteLine(); + + using (writer.IncreaseIndent()) + { + for (var i = 0; i < _members.Count; i++) + { + writer.WriteIndent(); + _members[i].Build(writer); + if (i != _members.Count - 1) + { + writer.Write(","); + } + + writer.WriteLine(); + } + } + + writer.WriteIndent(); + writer.Write(")"); + } + + if (_determineStatement) + { + writer.Write(";"); + writer.WriteLine(); + } + } +} diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/Builders/TupleBuilderExtensions.cs b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/TupleBuilderExtensions.cs new file mode 100644 index 0000000..1c7f1c8 --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/TupleBuilderExtensions.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; + +namespace StrawberryShake.CodeGeneration.CSharp.Builders; + +public static class TupleBuilderExtensions +{ + public static TupleBuilder AddMemberRange( + this TupleBuilder builder, + IEnumerable range) + { + foreach (var member in range) + { + builder.AddMember(member); + } + + return builder; + } + + public static TupleBuilder AddMemberRange( + this TupleBuilder builder, + IEnumerable range) + { + foreach (var member in range) + { + builder.AddMember(member); + } + + return builder; + } +} diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/Builders/TypeReferenceBuilder.cs b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/TypeReferenceBuilder.cs new file mode 100644 index 0000000..a8ed208 --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/TypeReferenceBuilder.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using HotChocolate.Language; + +namespace StrawberryShake.CodeGeneration.CSharp.Builders; + +public class TypeReferenceBuilder : ICode +{ + private readonly List _buildOrder = []; + private string? _name; + private readonly List _genericTypeArguments = []; + private bool _skipTrailingSpace; + + public static TypeReferenceBuilder New() + { + return new(); + } + + public TypeReferenceBuilder SetName(string name) + { + _name = name; + return this; + } + + public TypeReferenceBuilder SkipTrailingSpace() + { + _skipTrailingSpace = true; + return this; + } + + public TypeReferenceBuilder SetNameSpace(string @namespace) + { + return this; + } + + public TypeReferenceBuilder SetListType() + { + _buildOrder.Push(TypeKindToken.List); + return this; + } + + public TypeReferenceBuilder AddGeneric(string name) + { + _genericTypeArguments.Push(name); + return this; + } + + public TypeReferenceBuilder SetIsNullable(bool isNullable) + { + if (isNullable) + { + _buildOrder.Push(TypeKindToken.Nullable); + } + return this; + } + + private enum TypeKindToken + { + List, + Nullable, + } + + public override string ToString() + { + var text = new StringBuilder(); + using var stringWriter = new StringWriter(text); + using var codeWriter = new CodeWriter(stringWriter); + Build(codeWriter); + codeWriter.Flush(); + stringWriter.Flush(); + return text.ToString(); + } + + public void Build(CodeWriter writer) + { + HandleQueue(writer, 0); + if (!_skipTrailingSpace) + { + writer.WriteSpace(); + } + } + + private void HandleQueue(CodeWriter writer, int currentIndex) + { + if (currentIndex >= _buildOrder.Count) + { + writer.Write(_name); + if (_genericTypeArguments.Count > 0) + { + writer.Write("<"); + var next = false; + foreach (var generic in _genericTypeArguments) + { + if (next) + { + writer.Write(", "); + } + next = true; + + writer.Write(generic); + } + writer.Write(">"); + } + + return; + } + + var token = _buildOrder[currentIndex]; + switch (token) + { + case TypeKindToken.List: + writer.Write(TypeNames.GenericCollectionsNamespace + "IReadOnlyList<"); + HandleQueue(writer, currentIndex + 1); + writer.Write(">"); + break; + case TypeKindToken.Nullable: + HandleQueue(writer, currentIndex + 1); + writer.Write("?"); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } +} diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/Builders/XmlCommentBuilder.cs b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/XmlCommentBuilder.cs new file mode 100644 index 0000000..6def21e --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/Builders/XmlCommentBuilder.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.IO; + +namespace StrawberryShake.CodeGeneration.CSharp.Builders; + +public class XmlCommentBuilder : ICodeBuilder +{ + private string? _summary; + private List _code = []; + + public XmlCommentBuilder SetSummary(string summary) + { + _summary = summary; + return this; + } + + public XmlCommentBuilder AddCode(string code) + { + _code.Add(code); + return this; + } + + public static XmlCommentBuilder New() => new(); + + public void Build(CodeWriter writer) + { + if (_summary is not null) + { + writer.WriteIndentedLine("/// "); + WriteCommentLines(writer, _summary); + + foreach (var code in _code) + { + writer.WriteIndentedLine("/// "); + WriteCommentLines(writer, code); + writer.WriteIndentedLine("/// "); + } + + writer.WriteIndentedLine("/// "); + } + } + + private void WriteCommentLines(CodeWriter writer, string str) + { + using var reader = new StringReader(str); + var line = reader.ReadLine(); + do + { + if (line is not null) + { + writer.WriteIndent(); + writer.Write("/// "); + writer.Write(line); + writer.WriteLine(); + } + + line = reader.ReadLine(); + } + while (line is not null); + } +} diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/CodeGeneratorException.cs b/DragonFruit.Data.Roslyn/CodeGeneration/CodeGeneratorException.cs new file mode 100644 index 0000000..3883d4a --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/CodeGeneratorException.cs @@ -0,0 +1,5 @@ +using System; + +namespace StrawberryShake.CodeGeneration; + +public class CodeGeneratorException(string message) : Exception(message); diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/CodeWriter.cs b/DragonFruit.Data.Roslyn/CodeGeneration/CodeWriter.cs new file mode 100644 index 0000000..d20d80e --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/CodeWriter.cs @@ -0,0 +1,144 @@ +using System; +using System.IO; +using System.Text; + +namespace StrawberryShake.CodeGeneration; + +public class CodeWriter : TextWriter +{ + private readonly TextWriter _writer; + private readonly bool _disposeWriter; + private bool _disposed; + private int _indent; + + public CodeWriter(TextWriter writer) + { + _writer = writer; + _disposeWriter = false; + } + + public CodeWriter(StringBuilder text) + { + _writer = new StringWriter(text); + _disposeWriter = true; + } + + public override Encoding Encoding { get; } = Encoding.UTF8; + + public static string Indent { get; } = new(' ', 4); + + public override void Write(char value) => + _writer.Write(value); + + public void WriteStringValue(string value) + { + Write('"'); + Write(value); + Write('"'); + } + + public void WriteIndent() + { + if (_indent > 0) + { + var spaces = _indent * 4; + for (var i = 0; i < spaces; i++) + { + Write(' '); + } + } + } + + public string GetIndentString() + { + if (_indent > 0) + { + return new string(' ', _indent * 4); + } + return string.Empty; + } + + public void WriteIndentedLine(string format, params object?[] args) + { + WriteIndent(); + + if (args.Length == 0) + { + Write(format); + } + else + { + Write(format, args); + } + + WriteLine(); + } + + public void WriteSpace() => Write(' '); + + public IDisposable IncreaseIndent() + { + _indent++; + return new Block(DecreaseIndent); + } + + public void DecreaseIndent() + { + if (_indent > 0) + { + _indent--; + } + } + + public IDisposable WriteBraces() + { + WriteLeftBrace(); + WriteLine(); + +#pragma warning disable CA2000 + var indent = IncreaseIndent(); +#pragma warning restore CA2000 + + return new Block(() => + { + WriteLine(); + indent.Dispose(); + WriteIndent(); + WriteRightBrace(); + }); + } + + public void WriteLeftBrace() => Write('{'); + + public void WriteRightBrace() => Write('}'); + + public override void Flush() + { + base.Flush(); + _writer.Flush(); + } + + protected override void Dispose(bool disposing) + { + if (!_disposed && _disposeWriter) + { + if (disposing) + { + _writer.Dispose(); + } + _disposed = true; + } + } + + private sealed class Block : IDisposable + { + private readonly Action _decrease; + + public Block(Action close) + { + _decrease = close; + } + + public void Dispose() => _decrease(); + } +} diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/CodeWriterExtensions.cs b/DragonFruit.Data.Roslyn/CodeGeneration/CodeWriterExtensions.cs new file mode 100644 index 0000000..9f052b1 --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/CodeWriterExtensions.cs @@ -0,0 +1,33 @@ +using System; + +namespace StrawberryShake.CodeGeneration; + +public static class CodeWriterExtensions +{ + public static void WriteGeneratedAttribute(this CodeWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + var version = typeof(CodeWriter).Assembly.GetName().Version!.ToString(); + +#if DEBUG + writer.WriteIndentedLine( + "[global::System.CodeDom.Compiler.GeneratedCode(" + + "\"StrawberryShake\", \"11.0.0\")]"); +#else + writer.WriteIndentedLine( + "[global::System.CodeDom.Compiler.GeneratedCode(" + + $"\"StrawberryShake\", \"{version}\")]"); +#endif + } + + public static CodeWriter WriteComment(this CodeWriter writer, string comment) + { + writer.Write("// "); + writer.WriteLine(comment); + return writer; + } +} diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/Keywords.cs b/DragonFruit.Data.Roslyn/CodeGeneration/Keywords.cs new file mode 100644 index 0000000..9d1c78c --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/Keywords.cs @@ -0,0 +1,142 @@ +using System.Collections.Generic; + +namespace StrawberryShake.CodeGeneration.CSharp; + +public static class Keywords +{ + private static readonly HashSet _keywords = + [ + "abstract", + "as", + "base", + "bool", + "break", + "byte", + "case", + "catch", + "char", + "checked", + "class", + "const", + "continue", + "decimal", + "default", + "delegate", + "do", + "double", + "else", + "enum", + "event", + "explicit", + "extern", + "false", + "finally", + "fixed", + "float", + "for", + "foreach", + "goto", + "if", + "implicit", + "in", + "int", + "interface", + "internal", + "is", + "lock", + "long", + "namespace", + "new", + "null", + "object", + "operator", + "out", + "override", + "params", + "private", + "protected", + "public", + "readonly", + "record", + "ref", + "return", + "sbyte", + "sealed", + "short", + "sizeof", + "stackalloc", + "static", + "string", + "struct", + "switch", + "this", + "throw", + "true", + "try", + "typeof", + "uint", + "ulong", + "unchecked", + "unsafe", + "ushort", + "using", + "virtual", + "void", + "volatile", + "while", + "add", + "alias", + "ascending", + "async", + "await", + "by", + "descending", + "dynamic", + "equals", + "from", + "get", + "global", + "group", + "init", + "into", + "join", + "let", + "nameof", + "nint", + "notnull", + "nuint", + "on", + "orderby", + "partial", + "remove", + "select", + "set", + "unmanaged", + "value", + "var", + "when", + "where", + "with", + "yield", + ]; + + public static string ToSafeName(string name) + { + if (_keywords.Contains(name)) + { + return $"@{name}"; + } + + return name; + } + + public static string ToEscapedName(this string name) + { + if (_keywords.Contains(name)) + { + return $"@{name}"; + } + + return name; + } +} diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/ListExtensions.cs b/DragonFruit.Data.Roslyn/CodeGeneration/ListExtensions.cs new file mode 100644 index 0000000..921848b --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/ListExtensions.cs @@ -0,0 +1,101 @@ +using System.Collections.Generic; + +namespace HotChocolate.Language; + +/// +/// Provides Stack and Queue extensions for to +/// the visitor APIs. +/// +public static class ListExtensions +{ + public static T Pop(this IList list) + { + var lastIndex = list.Count - 1; + var p = list[lastIndex]; + list.RemoveAt(lastIndex); + return p; + } + + public static bool TryPop(this IList list, out T item) + { + if (list.Count > 0) + { + var lastIndex = list.Count - 1; + item = list[lastIndex]!; + list.RemoveAt(lastIndex); + return true; + } + else + { + item = default!; + return false; + } + } + + public static T Peek(this IList list) + { + var lastIndex = list.Count - 1; + return list[lastIndex]; + } + + public static bool TryPeek(this IList list, out T item) + { + if (list.Count > 0) + { + var lastIndex = list.Count - 1; + item = list[lastIndex]!; + return true; + } + + item = default; + return false; + } + + public static bool TryPeek( + this IList list, + int elements, + out T item) + { + if (list.Count >= elements) + { + var lastIndex = list.Count - elements; + item = list[lastIndex]!; + return true; + } + + item = default; + return false; + } + + public static T? PeekOrDefault(this IList list, T? defaultValue = default) + { + if (list.Count > 0) + { + var lastIndex = list.Count - 1; + return list[lastIndex]; + } + + return defaultValue; + } + + public static TSearch? PeekOrDefault(this IList list, TSearch? defaultValue = default) + { + if (list.Count > 0) + { + for (var i = list.Count - 1; i >= 0; i--) + { + if (list[i] is TSearch item) + { + return item; + } + } + } + + return defaultValue; + } + + public static void Push(this IList list, T item) + { + list.Add(item); + } +} diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/RuntimeTypeInfo.cs b/DragonFruit.Data.Roslyn/CodeGeneration/RuntimeTypeInfo.cs new file mode 100644 index 0000000..2b108be --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/RuntimeTypeInfo.cs @@ -0,0 +1,100 @@ +using System; +using System.Linq; +using StrawberryShake.CodeGeneration.CSharp; + +namespace StrawberryShake.CodeGeneration; + +public class RuntimeTypeInfo : IEquatable +{ + public RuntimeTypeInfo(string fullName, bool isValueType = false) + { + var parts = fullName.Split('.'); + Name = parts.Last(); + Namespace = string.Join(".", parts.Take(parts.Length - 1)); + IsValueType = isValueType; + + if (!Namespace.StartsWith("global::")) + { + Namespace = "global::" + Namespace; + } + } + + public RuntimeTypeInfo(string name, string @namespace, bool isValueType = false) + { + Name = name; + Namespace = @namespace; + IsValueType = isValueType; + + if (!Namespace.StartsWith("global::")) + { + Namespace = "global::" + Namespace; + } + } + + public string Name { get; } + + public string Namespace { get; } + + public string FullName => + Namespace == "global::" + ? Namespace + Name.ToEscapedName() + : Namespace + "." + Name.ToEscapedName(); + + public string NamespaceWithoutGlobal => + Namespace.Replace("global::", string.Empty); + + public bool IsValueType { get; } + + public bool Equals(RuntimeTypeInfo? other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return Name == other.Name && + Namespace == other.Namespace && + IsValueType == other.IsValueType; + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((RuntimeTypeInfo)obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = Name.GetHashCode(); + hashCode = (hashCode * 397) ^ Namespace.GetHashCode(); + hashCode = (hashCode * 397) ^ IsValueType.GetHashCode(); + return hashCode; + } + } + + public override string ToString() + { + return $"{Namespace}.{Name.ToEscapedName()}"; + } +} diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/TypeNames.cs b/DragonFruit.Data.Roslyn/CodeGeneration/TypeNames.cs new file mode 100644 index 0000000..1aba612 --- /dev/null +++ b/DragonFruit.Data.Roslyn/CodeGeneration/TypeNames.cs @@ -0,0 +1,65 @@ +// ReSharper disable InconsistentNaming + +namespace StrawberryShake.CodeGeneration; + +public static class TypeNames +{ + public const string IEquatable = "global::System.IEquatable"; + public const string Type = "global::System.Type"; + public const string Nullable = "global::System.Nullable"; + public const string JsonElement = "global::System.Text.Json.JsonElement"; + public const string JsonDocument = "global::System.Text.Json.JsonDocument"; + public const string JsonValueKind = "global::System.Text.Json.JsonValueKind"; + public const string JsonWriterOptions = "global::System.Text.Json.JsonWriterOptions"; + public const string Utf8JsonWriter = "global::System.Text.Json.Utf8JsonWriter"; + + public const string String = "global::System.String"; + public const string Byte = "global::System.Byte"; + public const string ByteArray = "global::System.Byte[]"; + public const string Array = "global::System.Array"; + public const string Int16 = "global::System.Int16"; + public const string Int32 = "global::System.Int32"; + public const string Int64 = "global::System.Int64"; + public const string UInt16 = "global::System.UInt16"; + public const string UInt32 = "global::System.UInt32"; + public const string UInt64 = "global::System.UInt64"; + public const string Single = "global::System.Single"; + public const string Double = "global::System.Double"; + public const string Decimal = "global::System.Decimal"; + public const string Uri = "global::System.Uri"; + public const string Boolean = "global::System.Boolean"; + public const string Object = "global::System.Object"; + public const string Guid = "global::System.Guid"; + public const string DateTime = "global::System.DateTime"; + public const string TimeSpan = "global::System.TimeSpan"; + public const string EncodingUtf8 = "global::System.Text.Encoding.UTF8"; + public const string List = GenericCollectionsNamespace + "List"; + public const string IEnumerable = GenericCollectionsNamespace + "IEnumerable"; + public const string Concat = "global::System.Linq.Enumerable.Concat"; + public const string IList = GenericCollectionsNamespace + "IList"; + + public const string IReadOnlyCollection = GenericCollectionsNamespace + "IReadOnlyCollection"; + + public const string IReadOnlyDictionary = GenericCollectionsNamespace + "IReadOnlyDictionary"; + + public const string IReadOnlyList = GenericCollectionsNamespace + "IReadOnlyList"; + public const string HashSet = GenericCollectionsNamespace + "HashSet"; + public const string ISet = GenericCollectionsNamespace + "ISet"; + public const string IReadOnlySpan = "global::System.ReadOnlySpan"; + public const string DateTimeOffset = "global::System.DateTimeOffset"; + public const string OrdinalStringComparison = "global::System.StringComparison.Ordinal"; + public const string Func = "global::System.Func"; + public const string Task = "global::System.Threading.Tasks.Task"; + public const string CancellationToken = "global::System.Threading.CancellationToken"; + public const string NotSupportedException = "global::System.NotSupportedException"; + public const string ArgumentNullException = "global::System.ArgumentNullException"; + public const string ArgumentException = "global::System.ArgumentException"; + + public const string ArgumentOutOfRangeException = "global::System.ArgumentOutOfRangeException"; + + public const string Exception = "global::System.Exception"; + + public const string GenericCollectionsNamespace = "global::System.Collections.Generic."; + public const string Dictionary = "global::System.Collections.Generic.Dictionary"; + public const string KeyValuePair = "global::System.Collections.Generic.KeyValuePair"; +} From 6f4fdc2433cc72465fab821d50f1681f3b9d039c Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 9 Aug 2024 11:09:54 +0100 Subject: [PATCH 09/14] set generated code to correct value --- .../CodeGeneration/CodeWriterExtensions.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/CodeWriterExtensions.cs b/DragonFruit.Data.Roslyn/CodeGeneration/CodeWriterExtensions.cs index 9f052b1..6d2d6ec 100644 --- a/DragonFruit.Data.Roslyn/CodeGeneration/CodeWriterExtensions.cs +++ b/DragonFruit.Data.Roslyn/CodeGeneration/CodeWriterExtensions.cs @@ -16,11 +16,11 @@ public static void WriteGeneratedAttribute(this CodeWriter writer) #if DEBUG writer.WriteIndentedLine( "[global::System.CodeDom.Compiler.GeneratedCode(" + - "\"StrawberryShake\", \"11.0.0\")]"); + "\"DragonFruit.Data\", \"4.0.0\")]"); #else - writer.WriteIndentedLine( - "[global::System.CodeDom.Compiler.GeneratedCode(" + - $"\"StrawberryShake\", \"{version}\")]"); + writer.WriteIndentedLine( + "[global::System.CodeDom.Compiler.GeneratedCode(" + + $"\"StrawberryShake\", \"{version}\")]"); #endif } From 46a9ee7fee9a9cec843b7876b9a1658dc774d909 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 9 Aug 2024 11:20:03 +0100 Subject: [PATCH 10/14] fix auto-generated comment showing at the bottom --- .../ApiRequestSourceBuilder.cs | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/DragonFruit.Data.Roslyn/ApiRequestSourceBuilder.cs b/DragonFruit.Data.Roslyn/ApiRequestSourceBuilder.cs index 88df67c..02c23b6 100644 --- a/DragonFruit.Data.Roslyn/ApiRequestSourceBuilder.cs +++ b/DragonFruit.Data.Roslyn/ApiRequestSourceBuilder.cs @@ -18,6 +18,11 @@ namespace DragonFruit.Data.Roslyn; internal static class ApiRequestSourceBuilder { + private static readonly SyntaxTriviaList AutoGeneratedComment = SyntaxFactory.TriviaList( + SyntaxFactory.Comment("// "), + SyntaxFactory.CarriageReturnLineFeed, + SyntaxFactory.CarriageReturnLineFeed); + private static readonly UsingDirectiveSyntax[] DefaultUsingStatements = [ SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("System")), @@ -31,7 +36,7 @@ public static SourceText Build(INamedTypeSymbol classSymbol, RequestSymbolMetada { // classes are partial by default var classBuilder = new ClassBuilder() - .SetName(classSymbol.Name) + .SetName(classSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)) .AddImplements("global::DragonFruit.Data.Requests.IRequestBuilder"); var serializerMethodParamBuilder = new ParameterBuilder() @@ -46,7 +51,7 @@ public static SourceText Build(INamedTypeSymbol classSymbol, RequestSymbolMetada // create a new UriBuilder called uriBuilder pulling in the uri from this.RequestPath var methodBodyBuilder = new CodeBlockBuilder() - .AddCode("var uriBuilder = new global::System.StringBuilder(this.RequestPath);") + .AddCode("var uriBuilder = new global::System.Text.StringBuilder(this.RequestPath);") .AddEmptyLine(); // process queries @@ -75,13 +80,13 @@ public static SourceText Build(INamedTypeSymbol classSymbol, RequestSymbolMetada break; case RequestBodyType.CustomBodySerialized: - methodBodyBuilder.AddCode($"var requestContentData = {metadata.BodyProperty.Accessor}"); + methodBodyBuilder.AddCode($"var requestContentData = {metadata.BodyProperty.Accessor};"); methodBodyBuilder.AddCode(new IfBuilder().SetCondition("requestContentData != null") .AddCode("request.Content = serializerResolver.Resolve(requestContentData.GetType(), global::DragonFruit.Data.Serializers.DataDirection.Out).Serialize(requestContentData);")); break; case RequestBodyType.FormUriEncoded: - methodBodyBuilder.AddCode("var formContentBuilder = new global::System.Text.StringBuilder();"); + methodBodyBuilder.AddCode("var formContentBuilder = new global::System.Text.StringBuilder();").AddEmptyLine(); WriteUriQueryBuilder(methodBodyBuilder, metadata.Properties[ParameterType.Form].OfType(), "formContentBuilder"); methodBodyBuilder.AddCode(new IfBuilder().SetCondition("formContentBuilder.Length > 0").AddCode("formContentBuilder.Length--;")); @@ -110,7 +115,7 @@ public static SourceText Build(INamedTypeSymbol classSymbol, RequestSymbolMetada case RequestSymbolType.Stream: { variableName = $"st{++counter}"; - methodBodyBuilder.AddCode($"var {variableName} = {symbol.Accessor}"); + methodBodyBuilder.AddCode($"var {variableName} = {symbol.Accessor};"); methodBodyBuilder.AddCode(new IfBuilder().SetCondition($"{variableName} != null").AddCode($"multipartContent.Add(new global::System.Net.Http.StreamContent({variableName}), \"{symbol.ParameterName}\");")); break; } @@ -223,22 +228,16 @@ public static SourceText Build(INamedTypeSymbol classSymbol, RequestSymbolMetada classBuilder.Build(writer); } - // add class with generated code annotation + // add class with generated code annotation added to method (not class) var tree = CSharpSyntaxTree.ParseText(SourceText.From(code.ToString())); var classNode = tree.GetRoot().DescendantNodes().OfType().Single(); - var compilerGeneratedAttribute = SyntaxFactory.Attribute(SyntaxFactory.ParseName("System.CodeDom.Compiler.GeneratedCodeAttribute")).WithArgumentList(SyntaxFactory.AttributeArgumentList(SyntaxFactory.SeparatedList(new[] - { - SyntaxFactory.AttributeArgument(SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal("DragonFruit.Data"))), - SyntaxFactory.AttributeArgument(SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal("4.1.0"))) - }))); - - classNode = classNode.AddAttributeLists(SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(compilerGeneratedAttribute))); + var ns = SyntaxFactory.NamespaceDeclaration(SyntaxFactory.ParseName(classSymbol.ContainingNamespace.ToDisplayString())); return SyntaxFactory.CompilationUnit() .AddUsings(DefaultUsingStatements) - .AddMembers(SyntaxFactory.NamespaceDeclaration(SyntaxFactory.ParseName(classSymbol.ContainingNamespace.ToDisplayString()))) - .AddMembers(classNode) - .WithLeadingTrivia(SyntaxFactory.Comment("// ")) + .AddMembers(ns.AddMembers(classNode)) + .NormalizeWhitespace() + .WithLeadingTrivia(AutoGeneratedComment) // needs to go at the bottom to ensure it's put at the top of the file .GetText(Encoding.UTF8); } From 211c8e3044022776572ba7912ea14e62c82c3f97 Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 9 Aug 2024 11:36:34 +0100 Subject: [PATCH 11/14] fix syntax warnings --- .../ApiRequestSourceBuilder.cs | 49 ++++++++++++++----- .../ApiRequestSourceGenerator.cs | 3 +- 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/DragonFruit.Data.Roslyn/ApiRequestSourceBuilder.cs b/DragonFruit.Data.Roslyn/ApiRequestSourceBuilder.cs index 02c23b6..6d67aed 100644 --- a/DragonFruit.Data.Roslyn/ApiRequestSourceBuilder.cs +++ b/DragonFruit.Data.Roslyn/ApiRequestSourceBuilder.cs @@ -32,7 +32,7 @@ internal static class ApiRequestSourceBuilder SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("DragonFruit.Data.Requests")) ]; - public static SourceText Build(INamedTypeSymbol classSymbol, RequestSymbolMetadata metadata, RequestBodyType requestBodyType) + public static SourceText Build(INamedTypeSymbol classSymbol, RequestSymbolMetadata metadata, RequestBodyType requestBodyType, bool requireNewKeyword = false) { // classes are partial by default var classBuilder = new ClassBuilder() @@ -50,15 +50,14 @@ public static SourceText Build(INamedTypeSymbol classSymbol, RequestSymbolMetada .AddParameter(serializerMethodParamBuilder); // create a new UriBuilder called uriBuilder pulling in the uri from this.RequestPath - var methodBodyBuilder = new CodeBlockBuilder() - .AddCode("var uriBuilder = new global::System.Text.StringBuilder(this.RequestPath);") - .AddEmptyLine(); + var methodBodyBuilder = new CodeBlockBuilder(); + var buildQueryString = metadata.Properties[ParameterType.Query].Any(); // process queries - if (metadata.Properties[ParameterType.Query].Any()) + if (buildQueryString) { - methodBodyBuilder.AddCode("var queryBuilder = new global::System.Text.StringBuilder();"); - methodBodyBuilder.AddEmptyLine(); + methodBodyBuilder.AddCode("var uriBuilder = new global::System.Text.StringBuilder(this.RequestPath);").AddEmptyLine(); + methodBodyBuilder.AddCode("var queryBuilder = new global::System.Text.StringBuilder();").AddEmptyLine(); WriteUriQueryBuilder(methodBodyBuilder, metadata.Properties[ParameterType.Query].OfType(), "queryBuilder"); @@ -69,8 +68,9 @@ public static SourceText Build(INamedTypeSymbol classSymbol, RequestSymbolMetada methodBodyBuilder.AddEmptyLine(); } - // create request body - methodBodyBuilder.AddCode("var request = new global::System.Net.Http.HttpRequestMessage(this.RequestMethod, uriBuilder.ToString());").AddEmptyLine(); + // create request body (w/ shortcut to reduce allocations when no query is generated) + var uriAccessor = buildQueryString ? "uriBuilder.ToString()" : "this.RequestPath"; + methodBodyBuilder.AddCode($"var request = new global::System.Net.Http.HttpRequestMessage(this.RequestMethod, {uriAccessor});").AddEmptyLine(); // process body content switch (requestBodyType) @@ -204,12 +204,29 @@ public static SourceText Build(INamedTypeSymbol classSymbol, RequestSymbolMetada .AddCode("request.Headers.Add(kvp.Key, kvp.Value);")); break; + // enums are the only thing here that may/may not be nullable case EnumSymbolMetadata enumSymbol: - headerHandler.AddCode($"request.Headers.Add(\"{symbol.ParameterName}\", global::DragonFruit.Data.Converters.EnumConverter.GetEnumValue({symbol.Accessor}, global::DragonFruit.Data.Requests.EnumOption.{enumSymbol.EnumOption}));"); - break; + var enumBlock = $"request.Headers.Add(\"{symbol.ParameterName}\", global::DragonFruit.Data.Converters.EnumConverter.GetEnumValue({symbol.Accessor}, global::DragonFruit.Data.Requests.EnumOption.{enumSymbol.EnumOption}));"; + + if (symbol.Nullable) + { + headerHandler.AddCode(enumBlock); + break; + } + + methodBodyBuilder.AddCode(enumBlock); + continue; default: - headerHandler.AddCode($"request.Headers.Add(\"{symbol.ParameterName}\", {symbol.Accessor}.ToString());"); + var block = $"request.Headers.Add(\"{symbol.ParameterName}\", {symbol.Accessor}.ToString());"; + + if (symbol.Nullable) + { + headerHandler.AddCode(block); + break; + } + + methodBodyBuilder.AddCode(block); break; } @@ -233,6 +250,14 @@ public static SourceText Build(INamedTypeSymbol classSymbol, RequestSymbolMetada var classNode = tree.GetRoot().DescendantNodes().OfType().Single(); var ns = SyntaxFactory.NamespaceDeclaration(SyntaxFactory.ParseName(classSymbol.ContainingNamespace.ToDisplayString())); + if (requireNewKeyword) + { + var method = classNode.DescendantNodes().OfType().Single(); + var newModifiers = method.Modifiers.Add(SyntaxFactory.Token(SyntaxKind.NewKeyword)); + + classNode = classNode.ReplaceNode(method, method.WithModifiers(newModifiers)); + } + return SyntaxFactory.CompilationUnit() .AddUsings(DefaultUsingStatements) .AddMembers(ns.AddMembers(classNode)) diff --git a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs index 24ca183..643e40a 100644 --- a/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs +++ b/DragonFruit.Data.Roslyn/ApiRequestSourceGenerator.cs @@ -74,7 +74,8 @@ private void Execute(Compilation compilation, ImmutableArray Date: Fri, 9 Aug 2024 11:53:27 +0100 Subject: [PATCH 12/14] fix missed string --- DragonFruit.Data.Roslyn/CodeGeneration/CodeWriterExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DragonFruit.Data.Roslyn/CodeGeneration/CodeWriterExtensions.cs b/DragonFruit.Data.Roslyn/CodeGeneration/CodeWriterExtensions.cs index 6d2d6ec..55646c6 100644 --- a/DragonFruit.Data.Roslyn/CodeGeneration/CodeWriterExtensions.cs +++ b/DragonFruit.Data.Roslyn/CodeGeneration/CodeWriterExtensions.cs @@ -20,7 +20,7 @@ public static void WriteGeneratedAttribute(this CodeWriter writer) #else writer.WriteIndentedLine( "[global::System.CodeDom.Compiler.GeneratedCode(" + - $"\"StrawberryShake\", \"{version}\")]"); + $"\"DragonFruit.Data\", \"{version}\")]"); #endif } From 6dc4affdc12d0213f33bc8b67cee76c26534d91e Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 9 Aug 2024 12:02:23 +0100 Subject: [PATCH 13/14] update deps --- .../DragonFruit.Data.Roslyn.Tests.csproj | 10 +++++----- .../DragonFruit.Data.Roslyn.csproj | 14 +++++++------- .../DragonFruit.Data.Serializers.Html.csproj | 2 +- .../DragonFruit.Data.Tests.csproj | 11 ++++++++--- 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/DragonFruit.Data.Roslyn.Tests/DragonFruit.Data.Roslyn.Tests.csproj b/DragonFruit.Data.Roslyn.Tests/DragonFruit.Data.Roslyn.Tests.csproj index 21be28b..bd38b82 100644 --- a/DragonFruit.Data.Roslyn.Tests/DragonFruit.Data.Roslyn.Tests.csproj +++ b/DragonFruit.Data.Roslyn.Tests/DragonFruit.Data.Roslyn.Tests.csproj @@ -6,12 +6,12 @@ - - - + + + - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj index e1af6a4..e9738b5 100644 --- a/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj +++ b/DragonFruit.Data.Roslyn/DragonFruit.Data.Roslyn.csproj @@ -7,14 +7,12 @@ false netstandard2.0 - CS8669,$(NoWarn) - DragonFruit.Data.Roslyn DragonFruit.Data.Roslyn + CS8669,$(NoWarn) false true - true false true @@ -38,7 +36,8 @@ - + + @@ -47,7 +46,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - + + \ No newline at end of file diff --git a/DragonFruit.Data.Serializers.Html/DragonFruit.Data.Serializers.Html.csproj b/DragonFruit.Data.Serializers.Html/DragonFruit.Data.Serializers.Html.csproj index f09d924..f27841e 100644 --- a/DragonFruit.Data.Serializers.Html/DragonFruit.Data.Serializers.Html.csproj +++ b/DragonFruit.Data.Serializers.Html/DragonFruit.Data.Serializers.Html.csproj @@ -11,7 +11,7 @@ - + diff --git a/DragonFruit.Data.Tests/DragonFruit.Data.Tests.csproj b/DragonFruit.Data.Tests/DragonFruit.Data.Tests.csproj index 2546aad..5be099c 100644 --- a/DragonFruit.Data.Tests/DragonFruit.Data.Tests.csproj +++ b/DragonFruit.Data.Tests/DragonFruit.Data.Tests.csproj @@ -4,12 +4,13 @@ false true net8.0 + true - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -20,4 +21,8 @@ + + + + From d6418d6dbfa80c12b23c05755244558ce1b5633d Mon Sep 17 00:00:00 2001 From: Albie Spriddell Date: Fri, 9 Aug 2024 12:02:38 +0100 Subject: [PATCH 14/14] add acknowledgements to readme --- DragonFruit.Data.Roslyn/readme.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/DragonFruit.Data.Roslyn/readme.md b/DragonFruit.Data.Roslyn/readme.md index 1b8ed44..285cd03 100644 --- a/DragonFruit.Data.Roslyn/readme.md +++ b/DragonFruit.Data.Roslyn/readme.md @@ -8,6 +8,8 @@ A Roslyn source-generator and code-analyzer for DragonFruit.Data DragonFruit.Data.Roslyn is a source-generator and code-analyzer for DragonFruit.Data that allows the request-building logic for `ApiRequest` classes to be generated at compile-time, rather than at runtime for each request. It also provides code-analysis to ensure attributes are applied correctly and design rules are followed. +> Some aspects of the source generator are based on [ChilliCream's StrawberryShake code generation tooling](https://github.com/ChilliCream/graphql-platform/tree/13.9.11/src/StrawberryShake/CodeGeneration/src/CodeGeneration.CSharp) (MIT Licensed) + ## Usage/Getting Started **Note: while semantic versioning is used, it is best to ensure the versions of `DragonFruit.Data` and `DragonFruit.Data.Roslyn` are the same.**