From edd3e6f7fdeccd17a66c5989c9d14614a63663c6 Mon Sep 17 00:00:00 2001 From: almostchristian Date: Wed, 20 Sep 2023 02:11:15 +0800 Subject: [PATCH] Refactored Generator to be more generic. Renamed to ConfigurationProcessor.Generator. Added support for suffixes in source generation. Improved handling of static/constant values for source generation. Improved handling of System.Type properties for source generation. --- ...igurationProcessor.DependencyInjection.sln | 28 +-- .../ServiceRegistrationExtensions.cs | 4 +- .../TestWebApiGenerator.csproj | 4 +- sample/TestWebApiGenerator/appsettings.json | 8 +- .../Implementation/CommonExtensions.cs | 28 ++- .../Implementation/StringArgumentValue.cs | 2 +- .../Parsing/ServiceRegistrationClass.cs | 14 -- .../Parsing/ServiceRegistrationMethod.cs | 12 - .../Parsing/SymbolVisibility.cs | 8 - .../ConfigurationProcessor.Generator.csproj} | 2 +- .../GenerateServiceRegistrationAttribute.cs | 16 +- .../README.md | 0 ...gurationProcessor.SourceGeneration.csproj} | 1 - .../Core/CoreCompatExtensions.cs | 17 +- .../Core/ResolutionContext.cs | 2 +- .../Emitter.cs | 30 ++- .../Generator.cs} | 15 +- .../Parser.cs | 60 ++--- .../Parsing/JsonConfigurationFileParser.cs | 9 +- .../Parsing/ServiceRegistrationClass.cs | 32 +++ .../Parsing/ServiceRegistrationMethod.cs | 34 +++ .../Parsing/SymbolVisibility.cs | 8 + .../Utility/DiagnosticDescriptorHelper.cs | 2 +- .../Utility/DiagnosticDescriptors.cs | 2 +- .../Utility/EmitContext.cs | 4 +- .../Utility/Helpers.cs | 81 ++++++- src/Directory.Build.props | 2 +- ...ocessor.SourceGeneration.UnitTests.csproj} | 2 +- .../DelegateMembers.cs | 25 +++ .../EmitterTests.cs | 211 +++++++++++++++++- 30 files changed, 523 insertions(+), 140 deletions(-) delete mode 100644 src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Parsing/ServiceRegistrationClass.cs delete mode 100644 src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Parsing/ServiceRegistrationMethod.cs delete mode 100644 src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Parsing/SymbolVisibility.cs rename src/{ConfigurationProcessor.DependencyInjection.Generator/ConfigurationProcessor.DependencyInjection.Generator.csproj => ConfigurationProcessor.Generator/ConfigurationProcessor.Generator.csproj} (90%) rename src/{ConfigurationProcessor.DependencyInjection.Generator => ConfigurationProcessor.Generator}/GenerateServiceRegistrationAttribute.cs (74%) rename src/{ConfigurationProcessor.DependencyInjection.Generator => ConfigurationProcessor.Generator}/README.md (100%) rename src/{ConfigurationProcessor.DependencyInjection.SourceGeneration/ConfigurationProcessor.DependencyInjection.SourceGeneration.csproj => ConfigurationProcessor.SourceGeneration/ConfigurationProcessor.SourceGeneration.csproj} (96%) rename src/{ConfigurationProcessor.DependencyInjection.SourceGeneration => ConfigurationProcessor.SourceGeneration}/Core/CoreCompatExtensions.cs (78%) rename src/{ConfigurationProcessor.DependencyInjection.SourceGeneration => ConfigurationProcessor.SourceGeneration}/Core/ResolutionContext.cs (96%) rename src/{ConfigurationProcessor.DependencyInjection.SourceGeneration => ConfigurationProcessor.SourceGeneration}/Emitter.cs (68%) rename src/{ConfigurationProcessor.DependencyInjection.SourceGeneration/RegistrationGenerator.cs => ConfigurationProcessor.SourceGeneration/Generator.cs} (89%) rename src/{ConfigurationProcessor.DependencyInjection.SourceGeneration => ConfigurationProcessor.SourceGeneration}/Parser.cs (91%) rename src/{ConfigurationProcessor.DependencyInjection.SourceGeneration => ConfigurationProcessor.SourceGeneration}/Parsing/JsonConfigurationFileParser.cs (93%) create mode 100644 src/ConfigurationProcessor.SourceGeneration/Parsing/ServiceRegistrationClass.cs create mode 100644 src/ConfigurationProcessor.SourceGeneration/Parsing/ServiceRegistrationMethod.cs create mode 100644 src/ConfigurationProcessor.SourceGeneration/Parsing/SymbolVisibility.cs rename src/{ConfigurationProcessor.DependencyInjection.SourceGeneration => ConfigurationProcessor.SourceGeneration}/Utility/DiagnosticDescriptorHelper.cs (91%) rename src/{ConfigurationProcessor.DependencyInjection.SourceGeneration => ConfigurationProcessor.SourceGeneration}/Utility/DiagnosticDescriptors.cs (98%) rename src/{ConfigurationProcessor.DependencyInjection.SourceGeneration => ConfigurationProcessor.SourceGeneration}/Utility/EmitContext.cs (98%) rename src/{ConfigurationProcessor.DependencyInjection.SourceGeneration => ConfigurationProcessor.SourceGeneration}/Utility/Helpers.cs (81%) rename tests/ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests/{ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests.csproj => ConfigurationProcessor.SourceGeneration.UnitTests.csproj} (91%) create mode 100644 tests/ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests/DelegateMembers.cs diff --git a/ConfigurationProcessor.DependencyInjection.sln b/ConfigurationProcessor.DependencyInjection.sln index 6bbcc18..b3f8662 100644 --- a/ConfigurationProcessor.DependencyInjection.sln +++ b/ConfigurationProcessor.DependencyInjection.sln @@ -37,13 +37,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleOutboxApi", "sample\S EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.Components", "sample\Sample.Components\Sample.Components.csproj", "{53B667D5-FB51-4C83-A040-31724EC96F30}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConfigurationProcessor.DependencyInjection.Generator", "src\ConfigurationProcessor.DependencyInjection.Generator\ConfigurationProcessor.DependencyInjection.Generator.csproj", "{AD8581E7-B99F-42D8-BBA9-39D631F1F496}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestWebApiGenerator", "sample\TestWebApiGenerator\TestWebApiGenerator.csproj", "{0945131E-BEAB-4EE0-86FE-A2C44300CCA1}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConfigurationProcessor.DependencyInjection.SourceGeneration", "src\ConfigurationProcessor.DependencyInjection.SourceGeneration\ConfigurationProcessor.DependencyInjection.SourceGeneration.csproj", "{06738AF5-221D-44C4-AD3D-3377CA4C1BEC}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConfigurationProcessor.SourceGeneration.UnitTests", "tests\ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests\ConfigurationProcessor.SourceGeneration.UnitTests.csproj", "{1FA1DC9D-FF01-4B92-9EB0-5551F4016553}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConfigurationProcessor.SourceGeneration", "src\ConfigurationProcessor.SourceGeneration\ConfigurationProcessor.SourceGeneration.csproj", "{D3FF19EC-056F-4A54-81C2-6399877A6258}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests", "tests\ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests\ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests.csproj", "{1FA1DC9D-FF01-4B92-9EB0-5551F4016553}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConfigurationProcessor.Generator", "src\ConfigurationProcessor.Generator\ConfigurationProcessor.Generator.csproj", "{BC10F1A0-C2BE-4D0D-B874-6A79B709CCDF}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -87,22 +87,22 @@ Global {53B667D5-FB51-4C83-A040-31724EC96F30}.Debug|Any CPU.Build.0 = Debug|Any CPU {53B667D5-FB51-4C83-A040-31724EC96F30}.Release|Any CPU.ActiveCfg = Release|Any CPU {53B667D5-FB51-4C83-A040-31724EC96F30}.Release|Any CPU.Build.0 = Release|Any CPU - {AD8581E7-B99F-42D8-BBA9-39D631F1F496}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AD8581E7-B99F-42D8-BBA9-39D631F1F496}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AD8581E7-B99F-42D8-BBA9-39D631F1F496}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AD8581E7-B99F-42D8-BBA9-39D631F1F496}.Release|Any CPU.Build.0 = Release|Any CPU {0945131E-BEAB-4EE0-86FE-A2C44300CCA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0945131E-BEAB-4EE0-86FE-A2C44300CCA1}.Debug|Any CPU.Build.0 = Debug|Any CPU {0945131E-BEAB-4EE0-86FE-A2C44300CCA1}.Release|Any CPU.ActiveCfg = Release|Any CPU {0945131E-BEAB-4EE0-86FE-A2C44300CCA1}.Release|Any CPU.Build.0 = Release|Any CPU - {06738AF5-221D-44C4-AD3D-3377CA4C1BEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {06738AF5-221D-44C4-AD3D-3377CA4C1BEC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {06738AF5-221D-44C4-AD3D-3377CA4C1BEC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {06738AF5-221D-44C4-AD3D-3377CA4C1BEC}.Release|Any CPU.Build.0 = Release|Any CPU {1FA1DC9D-FF01-4B92-9EB0-5551F4016553}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1FA1DC9D-FF01-4B92-9EB0-5551F4016553}.Debug|Any CPU.Build.0 = Debug|Any CPU {1FA1DC9D-FF01-4B92-9EB0-5551F4016553}.Release|Any CPU.ActiveCfg = Release|Any CPU {1FA1DC9D-FF01-4B92-9EB0-5551F4016553}.Release|Any CPU.Build.0 = Release|Any CPU + {D3FF19EC-056F-4A54-81C2-6399877A6258}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D3FF19EC-056F-4A54-81C2-6399877A6258}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D3FF19EC-056F-4A54-81C2-6399877A6258}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D3FF19EC-056F-4A54-81C2-6399877A6258}.Release|Any CPU.Build.0 = Release|Any CPU + {BC10F1A0-C2BE-4D0D-B874-6A79B709CCDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BC10F1A0-C2BE-4D0D-B874-6A79B709CCDF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BC10F1A0-C2BE-4D0D-B874-6A79B709CCDF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BC10F1A0-C2BE-4D0D-B874-6A79B709CCDF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -117,10 +117,10 @@ Global {81216638-2D84-40AB-8AAA-5CA8065079F7} = {14052F2C-4DA2-45FC-8F05-33B7AC728494} {E6D3E9E4-983D-4193-90FC-98E3F7C8B530} = {645ABE25-B876-4372-8712-C66AA34020FC} {53B667D5-FB51-4C83-A040-31724EC96F30} = {645ABE25-B876-4372-8712-C66AA34020FC} - {AD8581E7-B99F-42D8-BBA9-39D631F1F496} = {60E2AA27-F390-4152-9039-A05388E2D3D8} {0945131E-BEAB-4EE0-86FE-A2C44300CCA1} = {645ABE25-B876-4372-8712-C66AA34020FC} - {06738AF5-221D-44C4-AD3D-3377CA4C1BEC} = {60E2AA27-F390-4152-9039-A05388E2D3D8} {1FA1DC9D-FF01-4B92-9EB0-5551F4016553} = {14052F2C-4DA2-45FC-8F05-33B7AC728494} + {D3FF19EC-056F-4A54-81C2-6399877A6258} = {60E2AA27-F390-4152-9039-A05388E2D3D8} + {BC10F1A0-C2BE-4D0D-B874-6A79B709CCDF} = {60E2AA27-F390-4152-9039-A05388E2D3D8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2358CB49-45E6-4E83-85C1-F995C676A43F} diff --git a/sample/TestWebApiGenerator/ServiceRegistrationExtensions.cs b/sample/TestWebApiGenerator/ServiceRegistrationExtensions.cs index 3262390..f85d5f5 100644 --- a/sample/TestWebApiGenerator/ServiceRegistrationExtensions.cs +++ b/sample/TestWebApiGenerator/ServiceRegistrationExtensions.cs @@ -1,11 +1,11 @@ -using ConfigurationProcessor.DependencyInjection; +using ConfigurationProcessor; using OpenTelemetry.Trace; namespace TestWebApiGenerator; internal static partial class ServiceRegistrationExtensions { - [GenerateServiceRegistration("Services", ExcludedSections = new[] { "Hsts" })] + [GenerateServiceRegistration("Services", ExcludedSections = new[] { "Hsts" }, ImplicitSuffixes = new[] { "Instrumentation", "Exporter" })] public static partial void AddServicesFromConfiguration(this WebApplicationBuilder builder); // [GenerateServiceRegistration("Services")] diff --git a/sample/TestWebApiGenerator/TestWebApiGenerator.csproj b/sample/TestWebApiGenerator/TestWebApiGenerator.csproj index d6a2ec0..b1422fd 100644 --- a/sample/TestWebApiGenerator/TestWebApiGenerator.csproj +++ b/sample/TestWebApiGenerator/TestWebApiGenerator.csproj @@ -18,8 +18,8 @@ - - + + diff --git a/sample/TestWebApiGenerator/appsettings.json b/sample/TestWebApiGenerator/appsettings.json index d9ddfd1..756dee3 100644 --- a/sample/TestWebApiGenerator/appsettings.json +++ b/sample/TestWebApiGenerator/appsettings.json @@ -24,10 +24,10 @@ "EndpointsApiExplorer": true, "SwaggerGen": true, "OpenTelemetryTracing": { - "AspNetCoreInstrumentation": true, - "HttpClientInstrumentation": true, - "SqlClientInstrumentation": true, - "JaegerExporter": true + "AspNetCore": true, + "HttpClient": true, + "SqlClient": true, + "Jaeger": true } } } diff --git a/src/ConfigurationProcessor.Core/Implementation/CommonExtensions.cs b/src/ConfigurationProcessor.Core/Implementation/CommonExtensions.cs index 415bbe7..0f23e2e 100644 --- a/src/ConfigurationProcessor.Core/Implementation/CommonExtensions.cs +++ b/src/ConfigurationProcessor.Core/Implementation/CommonExtensions.cs @@ -114,6 +114,22 @@ public static List FindConfigurationExtensionMethods( IEnumerable? candidateNames, MethodFilter? filter) { + static bool IsDefined(MethodInfo method, Type attributeType) + { +#if Generator + try + { + return method.CustomAttributes.Any(x => x.AttributeType.FullName == attributeType.FullName); + } + catch (InvalidOperationException) + { + return false; + } +#else + return method.IsDefined(attributeType, false); +#endif + } + IReadOnlyCollection configurationAssemblies = resolutionContext.ConfigurationAssemblies; var interfaces = configType.GetInterfaces(); var scannedTypes = configurationAssemblies @@ -125,11 +141,13 @@ public static List FindConfigurationExtensionMethods( .Concat(interfaces.Select(t => t.GetTypeInfo())) .SelectMany(t => candidateNames != null ? candidateNames.SelectMany(n => t.GetDeclaredMethods(n)) : t.DeclaredMethods) .Where(m => filter == null || filter(m, key)) -#if Generator - .Where(m => m.IsPublic && (m.IsStatic || IsTypeCompatible(configType, m.DeclaringType) || interfaces.Contains(m.DeclaringType))) -#else - .Where(m => !m.IsDefined(typeof(CompilerGeneratedAttribute), false) && m.IsPublic && ((m.IsStatic && m.IsDefined(typeof(ExtensionAttribute), false)) || IsTypeCompatible(configType, m.DeclaringType) || interfaces.Contains(m.DeclaringType))) -#endif + .Where(m => + !IsDefined(m, typeof(CompilerGeneratedAttribute)) && // method must not be compiler generated and + m.IsPublic && // method must be public and + ( + (m.IsStatic && IsDefined(m, typeof(ExtensionAttribute))) || // method is an extension method + IsTypeCompatible(configType, m.DeclaringType) || // method is declared in the target object type + interfaces.Contains(m.DeclaringType))) // the method is declared in one of the implemented interfaces .Where(m => !m.IsStatic || configType.IsTypeCompatible(SafeGetParameters(m).ElementAtOrDefault(0)?.ParameterType)) // If static method, checks that the first parameter is same as the extension type .ToList(); diff --git a/src/ConfigurationProcessor.Core/Implementation/StringArgumentValue.cs b/src/ConfigurationProcessor.Core/Implementation/StringArgumentValue.cs index 89d9aed..40c1158 100644 --- a/src/ConfigurationProcessor.Core/Implementation/StringArgumentValue.cs +++ b/src/ConfigurationProcessor.Core/Implementation/StringArgumentValue.cs @@ -166,7 +166,7 @@ public StringArgumentValue(IConfigurationSection section, string providedValue, if (methodCandidate != null) { - if (typeof(MethodInfo) == toType) + if (typeof(MethodInfo) == toType || typeof(Delegate) == toType) { return methodCandidate; } diff --git a/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Parsing/ServiceRegistrationClass.cs b/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Parsing/ServiceRegistrationClass.cs deleted file mode 100644 index ec1837c..0000000 --- a/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Parsing/ServiceRegistrationClass.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace ConfigurationProcessor.DependencyInjection.SourceGeneration.Parsing; - -internal sealed class ServiceRegistrationClass -{ - public List Methods { get; } = new(); - - public string Keyword { get; init; } = string.Empty; - - public string Namespace { get; init; } = string.Empty; - - public string Name { get; init; } = string.Empty; - - public ServiceRegistrationClass? ParentClass { get; set; } -} \ No newline at end of file diff --git a/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Parsing/ServiceRegistrationMethod.cs b/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Parsing/ServiceRegistrationMethod.cs deleted file mode 100644 index 743a045..0000000 --- a/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Parsing/ServiceRegistrationMethod.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Microsoft.CodeAnalysis; - -namespace ConfigurationProcessor.DependencyInjection.SourceGeneration.Parsing; - -internal sealed record class ServiceRegistrationMethod(string Name, string Arguments, string Modifiers, IEnumerable> ConfigurationValues, string ConfigurationSectionName) -{ - public string UniqueName { get; set; } = string.Empty; - - public string? ServiceCollectionField { get; set; } - - public string? ConfigurationField { get; set; } -} \ No newline at end of file diff --git a/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Parsing/SymbolVisibility.cs b/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Parsing/SymbolVisibility.cs deleted file mode 100644 index 34fc9f3..0000000 --- a/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Parsing/SymbolVisibility.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace ConfigurationProcessor.DependencyInjection.SourceGeneration.Parsing; - -internal enum SymbolVisibility -{ - Public, - Private, - Internal, -} \ No newline at end of file diff --git a/src/ConfigurationProcessor.DependencyInjection.Generator/ConfigurationProcessor.DependencyInjection.Generator.csproj b/src/ConfigurationProcessor.Generator/ConfigurationProcessor.Generator.csproj similarity index 90% rename from src/ConfigurationProcessor.DependencyInjection.Generator/ConfigurationProcessor.DependencyInjection.Generator.csproj rename to src/ConfigurationProcessor.Generator/ConfigurationProcessor.Generator.csproj index 05520d1..e7c4786 100644 --- a/src/ConfigurationProcessor.DependencyInjection.Generator/ConfigurationProcessor.DependencyInjection.Generator.csproj +++ b/src/ConfigurationProcessor.Generator/ConfigurationProcessor.Generator.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/ConfigurationProcessor.DependencyInjection.Generator/GenerateServiceRegistrationAttribute.cs b/src/ConfigurationProcessor.Generator/GenerateServiceRegistrationAttribute.cs similarity index 74% rename from src/ConfigurationProcessor.DependencyInjection.Generator/GenerateServiceRegistrationAttribute.cs rename to src/ConfigurationProcessor.Generator/GenerateServiceRegistrationAttribute.cs index 006f5d3..dcba033 100644 --- a/src/ConfigurationProcessor.DependencyInjection.Generator/GenerateServiceRegistrationAttribute.cs +++ b/src/ConfigurationProcessor.Generator/GenerateServiceRegistrationAttribute.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace ConfigurationProcessor.DependencyInjection; +namespace ConfigurationProcessor; /// /// Attribute for generating service registration. @@ -33,6 +29,16 @@ public GenerateServiceRegistrationAttribute(string configurationSection) /// public string[] ExcludedSections { get; set; } = Array.Empty(); + /// + /// Subsections that are treated separately. + /// + public string[] ExpandableSections { get; set; } = Array.Empty(); + + /// + /// Suffixes that can be ommitted. + /// + public string[] ImplicitSuffixes { get; set; } = Array.Empty(); + /// /// Gets the configuration section. /// diff --git a/src/ConfigurationProcessor.DependencyInjection.Generator/README.md b/src/ConfigurationProcessor.Generator/README.md similarity index 100% rename from src/ConfigurationProcessor.DependencyInjection.Generator/README.md rename to src/ConfigurationProcessor.Generator/README.md diff --git a/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/ConfigurationProcessor.DependencyInjection.SourceGeneration.csproj b/src/ConfigurationProcessor.SourceGeneration/ConfigurationProcessor.SourceGeneration.csproj similarity index 96% rename from src/ConfigurationProcessor.DependencyInjection.SourceGeneration/ConfigurationProcessor.DependencyInjection.SourceGeneration.csproj rename to src/ConfigurationProcessor.SourceGeneration/ConfigurationProcessor.SourceGeneration.csproj index eb1bb27..61cd371 100644 --- a/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/ConfigurationProcessor.DependencyInjection.SourceGeneration.csproj +++ b/src/ConfigurationProcessor.SourceGeneration/ConfigurationProcessor.SourceGeneration.csproj @@ -27,7 +27,6 @@ - diff --git a/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Core/CoreCompatExtensions.cs b/src/ConfigurationProcessor.SourceGeneration/Core/CoreCompatExtensions.cs similarity index 78% rename from src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Core/CoreCompatExtensions.cs rename to src/ConfigurationProcessor.SourceGeneration/Core/CoreCompatExtensions.cs index 573812f..e9813ee 100644 --- a/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Core/CoreCompatExtensions.cs +++ b/src/ConfigurationProcessor.SourceGeneration/Core/CoreCompatExtensions.cs @@ -5,8 +5,21 @@ namespace ConfigurationProcessor.Core.Implementation; internal static class CoreCompatExtensions { - public static IConfigurationArgumentValue GetArgumentValue(this IConfigurationSection value, ResolutionContext resolutionContext) - => BlankConfigurationArgValue.Instance; + public static IConfigurationArgumentValue GetArgumentValue(this IConfigurationSection argumentSection, ResolutionContext resolutionContext) + { + IConfigurationArgumentValue argumentValue; + + if (argumentSection.Value != null) + { + argumentValue = new StringArgumentValue(argumentSection, argumentSection.Value, argumentSection.Key); + } + else + { + argumentValue = new ObjectArgumentValue(argumentSection); + } + + return argumentValue; + } internal static Dictionary Blank(this IConfigurationSection section) => new() diff --git a/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Core/ResolutionContext.cs b/src/ConfigurationProcessor.SourceGeneration/Core/ResolutionContext.cs similarity index 96% rename from src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Core/ResolutionContext.cs rename to src/ConfigurationProcessor.SourceGeneration/Core/ResolutionContext.cs index 0b46937..cb86fa1 100644 --- a/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Core/ResolutionContext.cs +++ b/src/ConfigurationProcessor.SourceGeneration/Core/ResolutionContext.cs @@ -1,5 +1,5 @@ using System.Reflection; -using ConfigurationProcessor.DependencyInjection.SourceGeneration.Utility; +using ConfigurationProcessor.SourceGeneration.Utility; using Microsoft.Extensions.Configuration; namespace ConfigurationProcessor.Core.Implementation; diff --git a/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Emitter.cs b/src/ConfigurationProcessor.SourceGeneration/Emitter.cs similarity index 68% rename from src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Emitter.cs rename to src/ConfigurationProcessor.SourceGeneration/Emitter.cs index 1f598d9..63d4684 100644 --- a/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Emitter.cs +++ b/src/ConfigurationProcessor.SourceGeneration/Emitter.cs @@ -1,15 +1,28 @@ using System.Reflection; -using ConfigurationProcessor.DependencyInjection.SourceGeneration.Parsing; -using ConfigurationProcessor.DependencyInjection.SourceGeneration.Utility; +using ConfigurationProcessor.SourceGeneration.Parsing; +using ConfigurationProcessor.SourceGeneration.Utility; using Microsoft.Extensions.Configuration; -namespace ConfigurationProcessor.DependencyInjection.SourceGeneration; +namespace ConfigurationProcessor.SourceGeneration; -internal class Emitter +/// +/// Generates code. +/// +public static class Emitter { + /// + /// The version string. + /// public static readonly string VersionString = typeof(Emitter).Assembly.GetCustomAttribute().InformationalVersion; - public string Emit(IReadOnlyList generateConfigurationClasses, List references, CancellationToken cancellationToken) + /// + /// Generates code from service class registrations. + /// + /// + /// + /// + /// + public static string Emit(IReadOnlyList generateConfigurationClasses, List references, CancellationToken cancellationToken) { var emitContext = new EmitContext(generateConfigurationClasses.First().Namespace, references); @@ -31,12 +44,13 @@ static partial class {{configClass.Name}} emitContext.IncreaseIndent(); foreach (var configMethod in configClass.Methods) { + emitContext.ImplicitSuffixes = configMethod.ImplicitSuffixes; var sectionName = configMethod.ConfigurationSectionName; string configSectionVariableName = "servicesSection"; emitContext.Write($$""" - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("ConfigurationProcessor.DependencyInjection.Generator", "{{VersionString}}")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("ConfigurationProcessor.Generator", "{{VersionString}}")] {{configMethod.Modifiers}} void {{configMethod.Name}}({{configMethod.Arguments}}) { var {{configSectionVariableName}} = {{configMethod.ConfigurationField}}.GetSection("{{sectionName}}"); @@ -47,7 +61,7 @@ static partial class {{configClass.Name}} """); emitContext.IncreaseIndent(); - BuildMethods(emitContext, configMethod.ConfigurationValues, sectionName, configMethod.ServiceCollectionField!, configSectionVariableName); + BuildMethods(emitContext, configMethod.ConfigurationValues, sectionName, configMethod.TargetField!, configSectionVariableName); emitContext.DecreaseIndent(); emitContext.Write("}"); } @@ -63,7 +77,7 @@ static partial class {{configClass.Name}} return emitContext.ToString(); } - private void BuildMethods(EmitContext emitContext, IEnumerable> configurationValues, string sectionName, string targetExpression, string configSectionVariableName) + private static void BuildMethods(EmitContext emitContext, IEnumerable> configurationValues, string sectionName, string targetExpression, string configSectionVariableName) { var configBuilder = new ConfigurationBuilder(); configBuilder.AddInMemoryCollection(configurationValues); diff --git a/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/RegistrationGenerator.cs b/src/ConfigurationProcessor.SourceGeneration/Generator.cs similarity index 89% rename from src/ConfigurationProcessor.DependencyInjection.SourceGeneration/RegistrationGenerator.cs rename to src/ConfigurationProcessor.SourceGeneration/Generator.cs index b33f2fc..8282aa4 100644 --- a/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/RegistrationGenerator.cs +++ b/src/ConfigurationProcessor.SourceGeneration/Generator.cs @@ -4,8 +4,8 @@ using System.Reflection; using System.Text; -using ConfigurationProcessor.DependencyInjection.SourceGeneration; -using ConfigurationProcessor.DependencyInjection.SourceGeneration.Parsing; +using ConfigurationProcessor.SourceGeneration; +using ConfigurationProcessor.SourceGeneration.Parsing; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; @@ -16,7 +16,7 @@ namespace ConfigurationProcessor; /// Generates method for registration based on an appsetting.json file. /// [Generator] -public class RegistrationGenerator : ISourceGenerator +public class Generator : ISourceGenerator { /// public void Initialize(GeneratorInitializationContext context) @@ -33,6 +33,10 @@ public void Execute(GeneratorExecutionContext context) return; } +#if DEBUG + System.Diagnostics.Debugger.Launch(); +#endif + var p = new Parser(context, context.ReportDiagnostic, context.CancellationToken); IReadOnlyList registrationClasses = p.GetServiceRegistrationClasses(receiver.ClassDeclarations); if (registrationClasses.Count > 0) @@ -42,10 +46,9 @@ public void Execute(GeneratorExecutionContext context) var mlc = new MetadataLoadContext(resolver); var references = context.Compilation.ExternalReferences.Select(x => mlc.LoadFromAssemblyPath(x.Display!)).ToList(); - var e = new Emitter(); - string result = e.Emit(registrationClasses, references, context.CancellationToken); + string result = Emitter.Emit(registrationClasses, references, context.CancellationToken); - context.AddSource("RegisterServices.g.cs", SourceText.From(result, Encoding.UTF8)); + context.AddSource($"{registrationClasses.First().Name}.g.cs", SourceText.From(result, Encoding.UTF8)); } } diff --git a/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Parser.cs b/src/ConfigurationProcessor.SourceGeneration/Parser.cs similarity index 91% rename from src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Parser.cs rename to src/ConfigurationProcessor.SourceGeneration/Parser.cs index 129d121..7792b21 100644 --- a/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Parser.cs +++ b/src/ConfigurationProcessor.SourceGeneration/Parser.cs @@ -1,19 +1,20 @@ using System.Collections.Immutable; using System.Data; using System.Diagnostics; -using ConfigurationProcessor.DependencyInjection.SourceGeneration.Parsing; -using ConfigurationProcessor.DependencyInjection.SourceGeneration.Utility; +using ConfigurationProcessor.SourceGeneration.Parsing; +using ConfigurationProcessor.SourceGeneration.Utility; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace ConfigurationProcessor.DependencyInjection.SourceGeneration; +namespace ConfigurationProcessor.SourceGeneration; internal class Parser { internal const string DefaultConfigurationFile = "appsettings.json"; - internal const string GenerateServiceRegistrationAttribute = "ConfigurationProcessor.DependencyInjection.GenerateServiceRegistrationAttribute"; + internal const string GenerateServiceRegistrationAttribute = "ConfigurationProcessor.GenerateServiceRegistrationAttribute"; internal const string ServiceCollectionTypeName = "Microsoft.Extensions.DependencyInjection.IServiceCollection"; + internal const string WebApplicationBuilderTypeName = "Microsoft.AspNetCore.Builder.WebApplicationBuilder"; private readonly GeneratorExecutionContext context; private readonly Action reportDiagnostic; private readonly CancellationToken cancellationToken; @@ -35,11 +36,7 @@ internal IReadOnlyList GetServiceRegistrationClasses(I } INamedTypeSymbol? serviceCollectionSymbol = context.Compilation.GetBestTypeByMetadataName(ServiceCollectionTypeName); - if (serviceCollectionSymbol == null) - { - // nothing to do if this type isn't available - return Array.Empty(); - } + INamedTypeSymbol? webApplicationBuilderSymbol = context.Compilation.GetBestTypeByMetadataName(WebApplicationBuilderTypeName); INamedTypeSymbol? configurationSymbol = context.Compilation.GetBestTypeByMetadataName("Microsoft.Extensions.Configuration.IConfiguration"); if (configurationSymbol == null) @@ -85,6 +82,7 @@ internal IReadOnlyList GetServiceRegistrationClasses(I Debug.Assert(configurationMethodSymbol != null, "configuration method is present."); (string configurationSection, string? configurationFile) = (string.Empty, null); string[] excluded = Array.Empty(); + string[] suffixes = Array.Empty(); foreach (AttributeListSyntax mal in method.AttributeLists) { foreach (AttributeSyntax ma in mal.Attributes) @@ -163,6 +161,10 @@ internal IReadOnlyList GetServiceRegistrationClasses(I var values = (ImmutableArray)GetItem(value)!; excluded = values.Select(x => $"{configurationSection}:{x.Value}").ToArray(); break; + case "ImplicitSuffixes": + values = (ImmutableArray)GetItem(value)!; + suffixes = values.Select(x => x.Value?.ToString()).Where(x => !string.IsNullOrEmpty(x)).ToArray()!; + break; } } } @@ -210,7 +212,7 @@ internal IReadOnlyList GetServiceRegistrationClasses(I } var lm = new ServiceRegistrationMethod(configurationMethodSymbol.Name, methodSignature, method.Modifiers.ToString(), configurationValues, configurationSection); - + lm.ImplicitSuffixes = suffixes; static string ToDisplay(IParameterSymbol parameter) { return $"global::{parameter.Type} {parameter.Name}"; @@ -267,7 +269,7 @@ static string ToDisplay(IParameterSymbol parameter) keepMethod = false; } - bool foundServiceCollection = false; + bool foundTarget = false; bool foundConfiguration = false; foreach (IParameterSymbol paramSymbol in configurationMethodSymbol.Parameters) { @@ -295,18 +297,18 @@ static string ToDisplay(IParameterSymbol parameter) Diag(DiagnosticDescriptors.InvalidGenerateConfigurationMethodParameterName, paramSymbol.Locations[0]); } - var matchesServiceCollection = IsBaseOrIdentity(paramTypeSymbol, serviceCollectionSymbol); + var notMatchesConfiguration = !IsBaseOrIdentity(paramTypeSymbol, configurationSymbol); var matchesConfiguration = IsBaseOrIdentity(paramTypeSymbol, configurationSymbol); - if (foundServiceCollection && matchesServiceCollection) + if (foundTarget && notMatchesConfiguration) { keepMethod = false; Diag(DiagnosticDescriptors.MultipleServiceCollectionParameter, paramSymbol.Locations[0]); break; } - else if (matchesServiceCollection) + else if (notMatchesConfiguration) { - foundServiceCollection = matchesServiceCollection; - lm.ServiceCollectionField = paramName; + foundTarget = notMatchesConfiguration; + lm.TargetField = paramName; } if (foundConfiguration && matchesConfiguration) @@ -322,7 +324,7 @@ static string ToDisplay(IParameterSymbol parameter) } } - if (keepMethod && !foundServiceCollection && !foundConfiguration && configurationMethodSymbol.Parameters.Length == 1) + if (keepMethod && !foundConfiguration && configurationMethodSymbol.Parameters.Length == 1) { // we check if the single parameter has public properties that are assignable to serviceCollection and configuration var paramSymbol = configurationMethodSymbol.Parameters[0]; @@ -349,12 +351,18 @@ static string ToDisplay(IParameterSymbol parameter) } else { + bool isWebAppBuilder = webApplicationBuilderSymbol != null && IsBaseOrIdentity(paramTypeSymbol, webApplicationBuilderSymbol); + if (isWebAppBuilder) + { + foundTarget = false; + } + var properties = paramTypeSymbol.GetMembers().OfType().ToArray(); foreach (var property in properties) { - var matchesServiceCollection = IsBaseOrIdentity(property.Type, serviceCollectionSymbol); + var matchesServiceCollection = isWebAppBuilder && serviceCollectionSymbol != null && IsBaseOrIdentity(property.Type, serviceCollectionSymbol); var matchesConfiguration = IsBaseOrIdentity(property.Type, configurationSymbol); - if (foundServiceCollection && matchesServiceCollection) + if (foundTarget && matchesServiceCollection) { keepMethod = false; Diag(DiagnosticDescriptors.MultipleServiceCollectionParameter, paramSymbol.Locations[0]); @@ -362,8 +370,8 @@ static string ToDisplay(IParameterSymbol parameter) } else if (matchesServiceCollection) { - foundServiceCollection = matchesServiceCollection; - lm.ServiceCollectionField = $"{paramName}.{property.Name}"; + foundTarget = matchesServiceCollection; + lm.TargetField = $"{paramName}.{property.Name}"; } if (foundConfiguration && matchesConfiguration) @@ -384,18 +392,18 @@ static string ToDisplay(IParameterSymbol parameter) if (keepMethod) { - if (isStatic && !foundServiceCollection) + if (isStatic && !foundTarget) { Diag(DiagnosticDescriptors.MissingGenerateConfigurationArgument, method.GetLocation(), lm.Name); keepMethod = false; } - else if (!isStatic && foundServiceCollection) + else if (!isStatic && foundTarget) { Diag(DiagnosticDescriptors.GenerateConfigurationMethodShouldBeStatic, method.GetLocation()); } - else if (!isStatic && !foundServiceCollection) + else if (!isStatic && !foundTarget) { - if (serviceCollectionField == null) + if (serviceCollectionField == null && serviceCollectionSymbol != null) { (serviceCollectionField, multipleServiceCollectionFields) = FindServiceCollectionField(sm, classDec, serviceCollectionSymbol); } @@ -412,7 +420,7 @@ static string ToDisplay(IParameterSymbol parameter) } else { - lm.ServiceCollectionField = serviceCollectionField; + lm.TargetField = serviceCollectionField; } } else if (!foundConfiguration) diff --git a/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Parsing/JsonConfigurationFileParser.cs b/src/ConfigurationProcessor.SourceGeneration/Parsing/JsonConfigurationFileParser.cs similarity index 93% rename from src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Parsing/JsonConfigurationFileParser.cs rename to src/ConfigurationProcessor.SourceGeneration/Parsing/JsonConfigurationFileParser.cs index 371af14..7f9a72a 100644 --- a/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Parsing/JsonConfigurationFileParser.cs +++ b/src/ConfigurationProcessor.SourceGeneration/Parsing/JsonConfigurationFileParser.cs @@ -2,12 +2,12 @@ using System.Text.Json; using Microsoft.Extensions.Configuration; -namespace ConfigurationProcessor.DependencyInjection.SourceGeneration.Parsing; +namespace ConfigurationProcessor.SourceGeneration.Parsing; /// /// This implementation is copied from Microsoft.Extensions.Configuration.Json /// -internal sealed class JsonConfigurationFileParser +public sealed class JsonConfigurationFileParser { private readonly Dictionary data = new Dictionary(StringComparer.OrdinalIgnoreCase); private readonly Stack paths = new Stack(); @@ -16,6 +16,11 @@ private JsonConfigurationFileParser() { } + /// + /// Parses a json input stream into key value pairs. + /// + /// + /// public static IDictionary Parse(Stream input) => new JsonConfigurationFileParser().ParseStream(input); diff --git a/src/ConfigurationProcessor.SourceGeneration/Parsing/ServiceRegistrationClass.cs b/src/ConfigurationProcessor.SourceGeneration/Parsing/ServiceRegistrationClass.cs new file mode 100644 index 0000000..dba714b --- /dev/null +++ b/src/ConfigurationProcessor.SourceGeneration/Parsing/ServiceRegistrationClass.cs @@ -0,0 +1,32 @@ +namespace ConfigurationProcessor.SourceGeneration.Parsing; + +/// +/// Represents a class that contains one or more methods to be code generated. +/// +public sealed class ServiceRegistrationClass +{ + /// + /// List of methods to be code generated. + /// + public List Methods { get; } = new(); + + /// + /// The csharp keyword for the class declaration e.g. 'class' or 'record'. + /// + public string Keyword { get; init; } = string.Empty; + + /// + /// The namespace of the class. + /// + public string Namespace { get; init; } = string.Empty; + + /// + /// The class name. + /// + public string Name { get; init; } = string.Empty; + + /// + /// The parent class if the class is nested. + /// + public ServiceRegistrationClass? ParentClass { get; set; } +} \ No newline at end of file diff --git a/src/ConfigurationProcessor.SourceGeneration/Parsing/ServiceRegistrationMethod.cs b/src/ConfigurationProcessor.SourceGeneration/Parsing/ServiceRegistrationMethod.cs new file mode 100644 index 0000000..a6ae342 --- /dev/null +++ b/src/ConfigurationProcessor.SourceGeneration/Parsing/ServiceRegistrationMethod.cs @@ -0,0 +1,34 @@ +using Microsoft.CodeAnalysis; + +namespace ConfigurationProcessor.SourceGeneration.Parsing; + +/// +/// Represents a method to be code generated. +/// +/// The method name. +/// +/// +/// +/// +public sealed record class ServiceRegistrationMethod(string Name, string Arguments, string Modifiers, IEnumerable> ConfigurationValues, string ConfigurationSectionName) +{ + /// + /// The unique method name. + /// + public string UniqueName { get; set; } = string.Empty; + + /// + /// The target field expression. + /// + public string? TargetField { get; set; } + + /// + /// The configuration field expression. + /// + public string? ConfigurationField { get; set; } + + /// + /// The implicit suffixes. + /// + public string[]? ImplicitSuffixes { get; set; } +} \ No newline at end of file diff --git a/src/ConfigurationProcessor.SourceGeneration/Parsing/SymbolVisibility.cs b/src/ConfigurationProcessor.SourceGeneration/Parsing/SymbolVisibility.cs new file mode 100644 index 0000000..bff64bb --- /dev/null +++ b/src/ConfigurationProcessor.SourceGeneration/Parsing/SymbolVisibility.cs @@ -0,0 +1,8 @@ +namespace ConfigurationProcessor.SourceGeneration.Parsing; + +internal enum SymbolVisibility +{ + Public, + Private, + Internal, +} \ No newline at end of file diff --git a/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Utility/DiagnosticDescriptorHelper.cs b/src/ConfigurationProcessor.SourceGeneration/Utility/DiagnosticDescriptorHelper.cs similarity index 91% rename from src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Utility/DiagnosticDescriptorHelper.cs rename to src/ConfigurationProcessor.SourceGeneration/Utility/DiagnosticDescriptorHelper.cs index 44581d2..ca26bdf 100644 --- a/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Utility/DiagnosticDescriptorHelper.cs +++ b/src/ConfigurationProcessor.SourceGeneration/Utility/DiagnosticDescriptorHelper.cs @@ -1,6 +1,6 @@ using Microsoft.CodeAnalysis; -namespace ConfigurationProcessor.DependencyInjection.SourceGeneration.Utility; +namespace ConfigurationProcessor.SourceGeneration.Utility; /// /// Helper methods for creating instances. diff --git a/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Utility/DiagnosticDescriptors.cs b/src/ConfigurationProcessor.SourceGeneration/Utility/DiagnosticDescriptors.cs similarity index 98% rename from src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Utility/DiagnosticDescriptors.cs rename to src/ConfigurationProcessor.SourceGeneration/Utility/DiagnosticDescriptors.cs index 12ebdd3..84a420e 100644 --- a/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Utility/DiagnosticDescriptors.cs +++ b/src/ConfigurationProcessor.SourceGeneration/Utility/DiagnosticDescriptors.cs @@ -1,6 +1,6 @@ using Microsoft.CodeAnalysis; -namespace ConfigurationProcessor.DependencyInjection.SourceGeneration.Utility; +namespace ConfigurationProcessor.SourceGeneration.Utility; internal static class DiagnosticDescriptors { diff --git a/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Utility/EmitContext.cs b/src/ConfigurationProcessor.SourceGeneration/Utility/EmitContext.cs similarity index 98% rename from src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Utility/EmitContext.cs rename to src/ConfigurationProcessor.SourceGeneration/Utility/EmitContext.cs index 3835fa7..cb1c1fe 100644 --- a/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Utility/EmitContext.cs +++ b/src/ConfigurationProcessor.SourceGeneration/Utility/EmitContext.cs @@ -2,7 +2,7 @@ using System.Reflection; using System.Text; -namespace ConfigurationProcessor.DependencyInjection.SourceGeneration.Utility; +namespace ConfigurationProcessor.SourceGeneration.Utility; internal record class EmitContext(string Namespace, List References) { @@ -15,6 +15,8 @@ internal record class EmitContext(string Namespace, List References) public Dictionary> TypeMap { get; } = References.SelectMany(x => x.GetExportedTypes()).GroupBy(x => x.FullName).ToDictionary(x => x.Key, x => x.ToList()); + public string[]? ImplicitSuffixes { get; set; } + public void AddNamespace(string ns) => namespaces.Add(ns); diff --git a/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Utility/Helpers.cs b/src/ConfigurationProcessor.SourceGeneration/Utility/Helpers.cs similarity index 81% rename from src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Utility/Helpers.cs rename to src/ConfigurationProcessor.SourceGeneration/Utility/Helpers.cs index d8d68ed..2945aa0 100644 --- a/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Utility/Helpers.cs +++ b/src/ConfigurationProcessor.SourceGeneration/Utility/Helpers.cs @@ -1,10 +1,11 @@ -using System.Reflection; +using System.Diagnostics; +using System.Reflection; using ConfigurationProcessor.Core.Implementation; -using ConfigurationProcessor.DependencyInjection.SourceGeneration.Parsing; +using ConfigurationProcessor.SourceGeneration.Parsing; using Microsoft.CodeAnalysis; using Microsoft.Extensions.Configuration; -namespace ConfigurationProcessor.DependencyInjection.SourceGeneration.Utility; +namespace ConfigurationProcessor.SourceGeneration.Utility; internal static class Helpers { @@ -119,7 +120,6 @@ public static void EmitComplex( string targetVariableName, IConfigurationSection configSection, string newSectionName, - string parentSectionName, IConfiguration rootConfiguration, Dictionary paramArgs) { @@ -166,6 +166,16 @@ public static void EmitComplex( emitContext.DecreaseIndent(); emitContext.Write("}"); } + else if (StringArgumentValue.TryParseStaticMemberAccessor(configSection.Value!, out var accessorTypeName, out var memberName)) + { + emitContext.Write($$""" + + if ({{configSectionVariableName}}.GetValue("{{configKey}}") == "{{configSection.Value}}") + { + {{targetVariableName}}.{{methodName}}({{accessorTypeName}}.{{memberName}}); + } + """); + } else { emitContext.Write(string.Empty); @@ -216,7 +226,6 @@ public static void EmitValues( configSection, typeArgs, paramArgs, - sectionName, configSectionVariableName, targetTypeName, targetVariableName); @@ -230,15 +239,25 @@ private static void EmitValues( IConfigurationSection configSection, TypeResolver[] typeArgs, Dictionary paramArgs, - string configSectionName, string configSectionVariableName, string targetTypeName, string targetVariableName) { Type targetType = emitContext.TypeMap[targetTypeName].Single(); var resolutionContext = new ResolutionContext(emitContext, rootConfiguration); + + var origCandidateNames = new[] { methodName, $"Add{methodName}", $"set_{methodName}" }; + var candidateNames = new List(origCandidateNames); + if (emitContext.ImplicitSuffixes != null && emitContext.ImplicitSuffixes.Length > 0) + { + foreach (var suffix in emitContext.ImplicitSuffixes) + { + candidateNames.AddRange(origCandidateNames.Select(x => $"{x}{suffix}")); + } + } + IEnumerable configurationMethods = resolutionContext - .FindConfigurationExtensionMethods(methodName, targetType, typeArgs, new[] { methodName, $"Add{methodName}", $"set_{methodName}" }, null); + .FindConfigurationExtensionMethods(methodName, targetType, typeArgs, candidateNames.ToArray(), (m, n) => candidateNames.Contains(n)); var suppliedArgumentNames = paramArgs?.Keys.ToArray() ?? Array.Empty(); @@ -361,16 +380,55 @@ private static void EmitValues( } } - if (targetType.GetProperty(methodName) is PropertyInfo propertyInfo) + var propertyInfo = targetType.GetProperty(methodName); + + Debug.Assert(propertyInfo != null || configurationMethod != null, "Configuration method not found and not a property"); + + if (propertyInfo != null) { if (paramArgs?.Count(x => !string.IsNullOrEmpty(x.Key)) > 0) { + // check if the property has an accessible setter and parameterless constructor + if (propertyInfo.CanWrite && propertyInfo.PropertyType.GetConstructor(Type.EmptyTypes) != null) + { + emitContext.Write( + $@"{targetVariableName}.{propertyInfo.Name} = new {propertyInfo.PropertyType.GetCSharpFullName()}();"); + } + foreach (var param in paramArgs) { if (propertyInfo.PropertyType.GetProperty(param.Key) is PropertyInfo subProperty) { - emitContext.Write( - $@"{targetVariableName}.{propertyInfo.Name}.{subProperty.Name} = {configSectionVariableName}.GetValue<{subProperty.PropertyType.GetCSharpFullName()}>(""{param.Value.ConfigSection.Key}"");"); + var value = param.Value.ConfigSection.GetSection(param.Key).Value; + if (subProperty.PropertyType == typeof(Type)) + { + emitContext.Write($$""" + if ({{configSectionVariableName}}.GetValue("{{param.Value.ConfigSection.Key}}:{{param.Key}}") == "{{value}}") + { + {{targetVariableName}}.{{propertyInfo.Name}}.{{subProperty.Name}} = typeof({{value}}); + } + else + { + {{targetVariableName}}.{{propertyInfo.Name}}.{{subProperty.Name}} = global::System.Type.GetType({{configSectionVariableName}}.GetValue("{{param.Value.ConfigSection.Key}}:{{param.Key}}")); + } + + """); + } + else if (StringArgumentValue.TryParseStaticMemberAccessor(value!, out var accessorTypeName, out var memberName)) + { + emitContext.Write($$""" + if ({{configSectionVariableName}}.GetValue("{{param.Value.ConfigSection.Key}}:{{param.Key}}") == "{{value}}") + { + {{targetVariableName}}.{{propertyInfo.Name}}.{{subProperty.Name}} = {{accessorTypeName}}.{{memberName}}; + } + + """); + } + else + { + emitContext.Write( + $@"{targetVariableName}.{propertyInfo.Name}.{subProperty.Name} = {configSectionVariableName}.GetValue<{subProperty.PropertyType.GetCSharpFullName()}>(""{param.Value.ConfigSection.Key}:{param.Key}"");"); + } } else if ( (propertyInfo.PropertyType.GetMethods().SingleOrDefault(x => x.Name == param.Key) ?? @@ -404,7 +462,6 @@ private static void EmitValues( targetVariableName, configSection, $"section{methodName}", - configSectionName, rootConfiguration, paramArgs.ToDictionary(x => x.Key, x => x.Value.ConfigSection)); } @@ -422,7 +479,7 @@ public static string GetCSharpFullName(this Type propertyType) } else { - return propertyType.FullName; + return propertyType.FullName.Replace('+', '.'); } } } diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 6daf169..b46d10b 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -10,7 +10,7 @@ - 0.2.2 + 0.3.0 $(Version)-beta.1 diff --git a/tests/ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests/ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests.csproj b/tests/ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests/ConfigurationProcessor.SourceGeneration.UnitTests.csproj similarity index 91% rename from tests/ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests/ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests.csproj rename to tests/ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests/ConfigurationProcessor.SourceGeneration.UnitTests.csproj index 4b93df8..436e21e 100644 --- a/tests/ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests/ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests.csproj +++ b/tests/ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests/ConfigurationProcessor.SourceGeneration.UnitTests.csproj @@ -31,7 +31,7 @@ - + diff --git a/tests/ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests/DelegateMembers.cs b/tests/ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests/DelegateMembers.cs new file mode 100644 index 0000000..61053d4 --- /dev/null +++ b/tests/ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests/DelegateMembers.cs @@ -0,0 +1,25 @@ +namespace ConfigurationProcessor.SourceGeneration.UnitTests; + +public class DelegateMembers +{ + public void NonStaticTestDelegate() + { + } + + public static void TestDelegate() + { + } + + public static void TestDelegateOverload(string value) + { + } + + public static void TestDelegateOverload(int value) + { + } + + public static bool TestDelegateOverload(string svalue = null, int ivalue = 0) + { + return true; + } +} \ No newline at end of file diff --git a/tests/ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests/EmitterTests.cs b/tests/ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests/EmitterTests.cs index 7cf5c5f..6fef78a 100644 --- a/tests/ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests/EmitterTests.cs +++ b/tests/ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests/EmitterTests.cs @@ -1,13 +1,18 @@ -using ConfigurationProcessor.DependencyInjection.SourceGeneration.Parsing; +using ConfigurationProcessor.SourceGeneration.Parsing; using Microsoft.CodeAnalysis; using Microsoft.Extensions.DependencyModel; +using Microsoft.Extensions.Options; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Text; +using TestDummies; -namespace ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests; +namespace ConfigurationProcessor.SourceGeneration.UnitTests; public class EmitterTests { + public static DummyDelegate DummyDelegateField = DelegateMembers.TestDelegate; + [Fact] public void WithObjectNotation_MapToExtensionMethodWithSingleStringParameterUsingStringValue_RegistersService() { @@ -19,6 +24,194 @@ public void WithObjectNotation_MapToExtensionMethodWithSingleStringParameterUsin @"services.AddSimpleString(servicesSection.GetValue(""SimpleString""));"); } + [Fact] + public void WithObjectNotation_MapStringArrayUsingArrayNotationWithSingleStringOverloadUsingArrayWithMultipleElements_RegistersWithArrayOverload() + { + TestConfig(""" + { + "DummyString": [ + "hello", + "world" + ] + } + """, + """ + var sectionDummyString = servicesSection.GetSection("DummyString"); + if (sectionDummyString.Exists()) + { + services.AddDummyString(sectionDummyString.Get()); + } + """); + } + + [Fact] + public void WithObjectNotation_MapStringArrayUsingArrayNotationWithSingleStringOverloadUsingArrayWithSingleElement_RegistersWithArrayOverload() + { + TestConfig(""" + { + "DummyString": [ + "hello" + ] + } + """, + """ + var sectionDummyString = servicesSection.GetSection("DummyString"); + if (sectionDummyString.Exists()) + { + services.AddDummyString(sectionDummyString.Get()); + } + """); + } + + [Fact] + public void WithObjectNotation_MapIntArrayDirectlyWithOverload_RegistersService() + { + TestConfig(""" + { + "DummyArray": [ + 1, + 2 + ] + } + """, + """ + var sectionDummyArray = servicesSection.GetSection("DummyArray"); + if (sectionDummyArray.Exists()) + { + services.AddDummyArray(sectionDummyArray.Get()); + } + """); + } + + [Fact] + public void WithObjectNotation_MapUsingStringDirectlyWithStringArrayOverload_RegistersWithSingleStringOverload() + { + TestConfig(""" + { + "DummyString": "hello" + } + """, + """ + services.AddDummyString(servicesSection.GetValue("DummyString")); + """); + } + + [Fact] + public void WithObjectNotation_MapToExtensionMethodWithSingleDelegateParameterDirectly_RegistersService() + { + TestConfig($$""" + { + "SimpleDelegate": "{{NameOf()}}::{{nameof(DelegateMembers.TestDelegate)}}" + } + """, + $$""" + if (servicesSection.GetValue("SimpleDelegate") == "{{NameOf()}}::{{nameof(DelegateMembers.TestDelegate)}}") + { + services.AddSimpleDelegate({{NameOf()}}.{{nameof(DelegateMembers.TestDelegate)}}); + } + """); + } + + [Theory] + [InlineData("Time")] + [InlineData("Time2")] + public void WithObjectNotation_MapToExtensionMethodAcceptingConfigurationActionDelegate_GeneratesConfigurationActionBasedOnObject(string timeProperty) + { + TestConfig($$""" + { + "ConfigurationAction": { + "Name": "hello", + "Value": { + "{{timeProperty}}" : "13:00:10", + "Location": "http://www.google.com", + "ContextType": "{{NameOf()}}", + "OnError": "{{NameOf()}}::{{nameof(DummyDelegateField)}}" + } + } + } + """, + $$""" + var sectionConfigurationAction = servicesSection.GetSection("ConfigurationAction"); + if (sectionConfigurationAction.Exists()) + { + services.AddConfigurationAction(options => + { + options.Value = new TestDummies.ComplexObject.ChildValue(); + if (sectionConfigurationAction.GetValue("Value:ContextType") == "{{NameOf()}}") + { + options.Value.ContextType = typeof({{NameOf()}}); + } + else + { + options.Value.ContextType = global::System.Type.GetType(sectionConfigurationAction.GetValue("Value:ContextType")); + } + + options.Value.Location = sectionConfigurationAction.GetValue("Value:Location"); + if (sectionConfigurationAction.GetValue("Value:OnError") == "{{NameOf()}}::{{nameof(DummyDelegateField)}}") + { + options.Value.OnError = {{NameOf()}}.{{nameof(DummyDelegateField)}}; + } + + options.Value.{{timeProperty}} = sectionConfigurationAction.GetValue("Value:{{timeProperty}}"); + options.Name = sectionConfigurationAction.GetValue("Name"); + }); + } + """); + } + + [Fact] + public void WithObjectNotation_MapToExtensionMethodAcceptingConfigurationActionDelegate_CanCallExtensionMethodsWithSingleStringParameterForConfigurationObjectWithString() + { + TestConfig(""" + { + "ConfigurationAction": { + "ConfigureName": "hello" + } + } + """, + """ + var sectionConfigurationAction = servicesSection.GetSection("ConfigurationAction"); + if (sectionConfigurationAction.Exists()) + { + services.AddConfigurationAction(options => + { + + options.AddConfigureName(sectionConfigurationAction.GetValue("ConfigureName")); + }); + } + """); + } + + + [Fact] + public void ConfigurationActionWithMethods() + { + TestConfig(""" + { + "ConfigurationAction": { + "SetName": { + "Name": "hello" + } + } + } + """, + """ + var sectionConfigurationAction = servicesSection.GetSection("ConfigurationAction"); + if (sectionConfigurationAction.Exists()) + { + services.AddConfigurationAction(options => + { + + var sectionSetName = sectionConfigurationAction.GetSection("SetName"); + if (sectionSetName.Exists()) + { + options.SetName(sectionSetName.GetValue("Name")); + } + }); + } + """); + } + [Fact] public void WithObjectNotation_MapToExtensionMethodWithSingleIntegerParameterUsingNumberValue_RegistersService() { @@ -180,10 +373,8 @@ public void WithObjectNotation_GivenConnectionString_SetsConnectionStringValue(s """); } - private static void TestConfig(string inputJsonFragment, string expectedCsharpFragment) + private static void TestConfig([StringSyntax(StringSyntaxAttribute.Json)] string inputJsonFragment, string expectedCsharpFragment) { - var emitter = new Emitter(); - var inputJson = $$""" { "Services" : {{inputJsonFragment}} @@ -201,12 +392,12 @@ private static void TestConfig(string inputJsonFragment, string expectedCsharpFr rc.Methods.Add(new ServiceRegistrationMethod("Register", "this IServiceCollection services, IConfiguration configuration", "public partial void", configurationValues, "Services") { ConfigurationField = "configuration", - ServiceCollectionField = "services", + TargetField = "services", }); var assemblies = GetLoadedAssemblies(); - var generatedCsharp = emitter.Emit(new[] { rc }, assemblies, default); + var generatedCsharp = Emitter.Emit(new[] { rc }, assemblies, default); var expectedCsharp = $$""" // using TestDummies; @@ -215,7 +406,7 @@ namespace TestApp { static partial class Test { - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("ConfigurationProcessor.DependencyInjection.Generator", "{{Emitter.VersionString}}")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("ConfigurationProcessor.Generator", "{{Emitter.VersionString}}")] public partial void void Register(this IServiceCollection services, IConfiguration configuration) { var servicesSection = configuration.GetSection("Services"); @@ -263,4 +454,6 @@ private static string IndentLines(string input, string indent) return sb.ToString().TrimEnd(); } -} \ No newline at end of file + + internal static string NameOf() => typeof(T).FullName; +}