From 3906b2952c9cd8e072efc808f6b8954b542239cc Mon Sep 17 00:00:00 2001 From: almostchristian Date: Wed, 6 Sep 2023 01:46:17 +0800 Subject: [PATCH] Refactored source generation code for testability. Reduce required libraries in source generator. --- ...igurationProcessor.DependencyInjection.sln | 19 ++- .../ServiceRegistrationExtensions.cs | 92 +------------ .../TestWebApiGenerator.csproj | 6 +- ...essor.DependencyInjection.Generator.csproj | 12 +- ...pendencyInjection.SourceGeneration.csproj} | 9 +- .../Core/CoreCompatExtensions.cs | 0 .../Core}/ResolutionContext.cs | 2 +- .../Emitter.cs | 47 ++----- .../Parser.cs | 39 ++++-- .../Parsing/JsonConfigurationFileParser.cs | 123 ++++++++++++++++++ .../Parsing/ServiceRegistrationClass.cs | 2 +- .../Parsing/ServiceRegistrationMethod.cs | 4 +- .../Parsing/SymbolVisibility.cs | 8 ++ .../RegistrationGenerator.cs} | 19 ++- .../Utility/DiagnosticDescriptorHelper.cs | 2 +- .../Utility/DiagnosticDescriptors.cs | 2 +- .../Utility/EmitContext.cs | 2 +- .../Utility/Helpers.cs | 8 +- .../Parsing/SymbolVisibility.cs | 8 -- src/Directory.Build.props | 4 +- ...njection.SourceGeneration.UnitTests.csproj | 39 ++++++ .../EmitterTests.cs | 101 ++++++++++++++ .../Usings.cs | 1 + 23 files changed, 356 insertions(+), 193 deletions(-) rename src/{ConfigurationProcessor.Gen.DependencyInjection/ConfigurationProcessor.Gen.DependencyInjection.csproj => ConfigurationProcessor.DependencyInjection.SourceGeneration/ConfigurationProcessor.DependencyInjection.SourceGeneration.csproj} (70%) rename src/{ConfigurationProcessor.Gen.DependencyInjection => ConfigurationProcessor.DependencyInjection.SourceGeneration}/Core/CoreCompatExtensions.cs (100%) rename src/{ConfigurationProcessor.Gen.DependencyInjection => ConfigurationProcessor.DependencyInjection.SourceGeneration/Core}/ResolutionContext.cs (96%) rename src/{ConfigurationProcessor.Gen.DependencyInjection => ConfigurationProcessor.DependencyInjection.SourceGeneration}/Emitter.cs (53%) rename src/{ConfigurationProcessor.Gen.DependencyInjection => ConfigurationProcessor.DependencyInjection.SourceGeneration}/Parser.cs (93%) create mode 100644 src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Parsing/JsonConfigurationFileParser.cs rename src/{ConfigurationProcessor.Gen.DependencyInjection => ConfigurationProcessor.DependencyInjection.SourceGeneration}/Parsing/ServiceRegistrationClass.cs (81%) rename src/{ConfigurationProcessor.Gen.DependencyInjection => ConfigurationProcessor.DependencyInjection.SourceGeneration}/Parsing/ServiceRegistrationMethod.cs (55%) create mode 100644 src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Parsing/SymbolVisibility.cs rename src/{ConfigurationProcessor.Gen.DependencyInjection/ConfigurationRegistrationGenerator.cs => ConfigurationProcessor.DependencyInjection.SourceGeneration/RegistrationGenerator.cs} (80%) rename src/{ConfigurationProcessor.Gen.DependencyInjection => ConfigurationProcessor.DependencyInjection.SourceGeneration}/Utility/DiagnosticDescriptorHelper.cs (91%) rename src/{ConfigurationProcessor.Gen.DependencyInjection => ConfigurationProcessor.DependencyInjection.SourceGeneration}/Utility/DiagnosticDescriptors.cs (98%) rename src/{ConfigurationProcessor.Gen.DependencyInjection => ConfigurationProcessor.DependencyInjection.SourceGeneration}/Utility/EmitContext.cs (98%) rename src/{ConfigurationProcessor.Gen.DependencyInjection => ConfigurationProcessor.DependencyInjection.SourceGeneration}/Utility/Helpers.cs (98%) delete mode 100644 src/ConfigurationProcessor.Gen.DependencyInjection/Parsing/SymbolVisibility.cs create mode 100644 tests/ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests/ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests.csproj create mode 100644 tests/ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests/EmitterTests.cs create mode 100644 tests/ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests/Usings.cs diff --git a/ConfigurationProcessor.DependencyInjection.sln b/ConfigurationProcessor.DependencyInjection.sln index 12e2b9f..6bbcc18 100644 --- a/ConfigurationProcessor.DependencyInjection.sln +++ b/ConfigurationProcessor.DependencyInjection.sln @@ -41,7 +41,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConfigurationProcessor.Depe 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.Gen.DependencyInjection", "src\ConfigurationProcessor.Gen.DependencyInjection\ConfigurationProcessor.Gen.DependencyInjection.csproj", "{E22B4D07-D154-480D-B27A-7B64E14669D4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConfigurationProcessor.DependencyInjection.SourceGeneration", "src\ConfigurationProcessor.DependencyInjection.SourceGeneration\ConfigurationProcessor.DependencyInjection.SourceGeneration.csproj", "{06738AF5-221D-44C4-AD3D-3377CA4C1BEC}" +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}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -93,10 +95,14 @@ Global {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 - {E22B4D07-D154-480D-B27A-7B64E14669D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E22B4D07-D154-480D-B27A-7B64E14669D4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E22B4D07-D154-480D-B27A-7B64E14669D4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E22B4D07-D154-480D-B27A-7B64E14669D4}.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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -113,7 +119,8 @@ Global {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} - {E22B4D07-D154-480D-B27A-7B64E14669D4} = {60E2AA27-F390-4152-9039-A05388E2D3D8} + {06738AF5-221D-44C4-AD3D-3377CA4C1BEC} = {60E2AA27-F390-4152-9039-A05388E2D3D8} + {1FA1DC9D-FF01-4B92-9EB0-5551F4016553} = {14052F2C-4DA2-45FC-8F05-33B7AC728494} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2358CB49-45E6-4E83-85C1-F995C676A43F} diff --git a/sample/TestWebApiGenerator/ServiceRegistrationExtensions.cs b/sample/TestWebApiGenerator/ServiceRegistrationExtensions.cs index a7f52ea..e5f0151 100644 --- a/sample/TestWebApiGenerator/ServiceRegistrationExtensions.cs +++ b/sample/TestWebApiGenerator/ServiceRegistrationExtensions.cs @@ -8,94 +8,6 @@ internal static partial class ServiceRegistrationExtensions [GenerateServiceRegistration("Services")] public static partial void AddServicesFromConfiguration(this WebApplicationBuilder builder); - //[GenerateServiceRegistration("Services")] - //internal static partial void AddServicesFromConfiguration(this IServiceCollection services, IConfiguration configurationb); -} - -/// -/// Target codegen output. -/// -static partial class ServiceRegistrationExtensions -{ - //public static partial void AddServicesFromConfiguration(this WebApplicationBuilder app) - // => app.Services.AddServicesFromConfiguration(app.Configuration); - - public static void AddServicesFromConfigurationX(this IServiceCollection services, IConfiguration configuration) - { - var servicesSection = configuration.GetSection("Services"); - if (!servicesSection.Exists()) - { - return; - } - - if (servicesSection.GetValue("Logging")) - { - services.AddLogging(); - } - - var hstsSection = servicesSection.GetSection("Hsts"); - if (hstsSection.Exists()) - { - services.AddHsts(x => - { - if (hstsSection.GetValue("ExcludedHosts:Clear")) - { - x.ExcludedHosts.Clear(); - } - - x.Preload = hstsSection.GetValue("Preload"); - x.IncludeSubDomains = hstsSection.GetValue("IncludeSubDomains"); - x.MaxAge = TimeSpan.Parse(hstsSection.GetValue("MaxAge")); - }); - } - - var configureSection = servicesSection.GetSection("Configure"); - if (configureSection.Exists()) - { - services.Configure(options => - { - options.HttpOnly = configureSection.GetValue("HttpOnly"); - options.Secure = configureSection.GetValue("Secure"); - }); - } - - if (servicesSection.GetValue("Controllers")) - { - services.AddControllers(); - } - - if (servicesSection.GetValue("EndpointsApiExplorer")) - { - services.AddEndpointsApiExplorer(); - } - - if (servicesSection.GetValue("SwaggerGen")) - { - services.AddSwaggerGen(); - } - - var openTelemetrySection = servicesSection.GetSection("OpenTelemetryTracing"); - services.AddOpenTelemetryTracing(options => - { - if (openTelemetrySection.GetValue("AspNetCoreInstrumentation")) - { - options.AddAspNetCoreInstrumentation(); - } - - if (openTelemetrySection.GetValue("HttpClientInstrumentation")) - { - options.AddHttpClientInstrumentation(); - } - - if (openTelemetrySection.GetValue("SqlClientInstrumentation")) - { - options.AddSqlClientInstrumentation(); - } - - if (openTelemetrySection.GetValue("JaegerExporter")) - { - options.AddJaegerExporter(); - } - }); - } + // [GenerateServiceRegistration("Services")] + // internal static partial void AddServicesFromConfiguration(this IServiceCollection services, IConfiguration configuration); } diff --git a/sample/TestWebApiGenerator/TestWebApiGenerator.csproj b/sample/TestWebApiGenerator/TestWebApiGenerator.csproj index dc2a457..0628a3a 100644 --- a/sample/TestWebApiGenerator/TestWebApiGenerator.csproj +++ b/sample/TestWebApiGenerator/TestWebApiGenerator.csproj @@ -5,8 +5,8 @@ net7.0 enable enable - true - $(BaseIntermediateOutputPath)\GeneratedFiles + @@ -20,7 +20,7 @@ - + diff --git a/src/ConfigurationProcessor.DependencyInjection.Generator/ConfigurationProcessor.DependencyInjection.Generator.csproj b/src/ConfigurationProcessor.DependencyInjection.Generator/ConfigurationProcessor.DependencyInjection.Generator.csproj index d812a1e..310ff0c 100644 --- a/src/ConfigurationProcessor.DependencyInjection.Generator/ConfigurationProcessor.DependencyInjection.Generator.csproj +++ b/src/ConfigurationProcessor.DependencyInjection.Generator/ConfigurationProcessor.DependencyInjection.Generator.csproj @@ -5,21 +5,17 @@ 11 true enable - 0.1.0-beta.5 + 0.1.0-beta.6 $(PackageTags);source generation Generator - + - - - - @@ -27,13 +23,9 @@ - - - - diff --git a/src/ConfigurationProcessor.Gen.DependencyInjection/ConfigurationProcessor.Gen.DependencyInjection.csproj b/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/ConfigurationProcessor.DependencyInjection.SourceGeneration.csproj similarity index 70% rename from src/ConfigurationProcessor.Gen.DependencyInjection/ConfigurationProcessor.Gen.DependencyInjection.csproj rename to src/ConfigurationProcessor.DependencyInjection.SourceGeneration/ConfigurationProcessor.DependencyInjection.SourceGeneration.csproj index 7ad475c..eb1bb27 100644 --- a/src/ConfigurationProcessor.Gen.DependencyInjection/ConfigurationProcessor.Gen.DependencyInjection.csproj +++ b/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/ConfigurationProcessor.DependencyInjection.SourceGeneration.csproj @@ -27,14 +27,11 @@ + - - - - @@ -47,12 +44,8 @@ - - - - diff --git a/src/ConfigurationProcessor.Gen.DependencyInjection/Core/CoreCompatExtensions.cs b/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Core/CoreCompatExtensions.cs similarity index 100% rename from src/ConfigurationProcessor.Gen.DependencyInjection/Core/CoreCompatExtensions.cs rename to src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Core/CoreCompatExtensions.cs diff --git a/src/ConfigurationProcessor.Gen.DependencyInjection/ResolutionContext.cs b/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Core/ResolutionContext.cs similarity index 96% rename from src/ConfigurationProcessor.Gen.DependencyInjection/ResolutionContext.cs rename to src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Core/ResolutionContext.cs index 0107dde..0b46937 100644 --- a/src/ConfigurationProcessor.Gen.DependencyInjection/ResolutionContext.cs +++ b/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Core/ResolutionContext.cs @@ -1,5 +1,5 @@ using System.Reflection; -using ConfigurationProcessor.Gen.DependencyInjection.Utility; +using ConfigurationProcessor.DependencyInjection.SourceGeneration.Utility; using Microsoft.Extensions.Configuration; namespace ConfigurationProcessor.Core.Implementation; diff --git a/src/ConfigurationProcessor.Gen.DependencyInjection/Emitter.cs b/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Emitter.cs similarity index 53% rename from src/ConfigurationProcessor.Gen.DependencyInjection/Emitter.cs rename to src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Emitter.cs index af2fcd0..a0cccb7 100644 --- a/src/ConfigurationProcessor.Gen.DependencyInjection/Emitter.cs +++ b/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Emitter.cs @@ -1,34 +1,24 @@ using System.Reflection; -using ConfigurationProcessor.Gen.DependencyInjection.Parsing; -using ConfigurationProcessor.Gen.DependencyInjection.Utility; +using ConfigurationProcessor.DependencyInjection.SourceGeneration.Parsing; +using ConfigurationProcessor.DependencyInjection.SourceGeneration.Utility; using Microsoft.CodeAnalysis; using Microsoft.Extensions.Configuration; -namespace ConfigurationProcessor.Gen.DependencyInjection; +namespace ConfigurationProcessor.DependencyInjection.SourceGeneration; internal class Emitter { - private GeneratorExecutionContext context; - private readonly Action reportDiagnostic; - - public Emitter(GeneratorExecutionContext context, Action reportDiagnostic) - { - this.context = context; - this.reportDiagnostic = reportDiagnostic; - } - - public string Emit(IReadOnlyList generateConfigurationClasses, CancellationToken cancellationToken) + public string Emit(IReadOnlyList generateConfigurationClasses, List references, CancellationToken cancellationToken) { - var paths = context.Compilation.ExternalReferences.Select(x => x.Display!).ToList(); - var resolver = new PathAssemblyResolver(paths); - var mlc = new MetadataLoadContext(resolver); - - var references = context.Compilation.ExternalReferences.Select(x => mlc.LoadFromAssemblyPath(x.Display!)).ToList(); - var emitContext = new EmitContext(generateConfigurationClasses.First().Namespace, references); foreach (var configClass in generateConfigurationClasses) { + if (cancellationToken.IsCancellationRequested) + { + break; + } + emitContext.Write($@" namespace {configClass.Namespace} {{ @@ -40,14 +30,6 @@ static partial class {configClass.Name} foreach (var configMethod in configClass.Methods) { var sectionName = configMethod.ConfigurationSectionName; - var configFile = configMethod.FileName; - - var jsonFile = context.AdditionalFiles.FirstOrDefault(x => Path.GetFileName(x.Path) == configFile); - if (jsonFile == null) - { - Diag(DiagnosticDescriptors.ConfigurationFileNotFound, configMethod.Location, configMethod.FileName); - continue; - } string configSectionVariableName = "servicesSection"; @@ -62,7 +44,7 @@ static partial class {configClass.Name} }}"); emitContext.IncreaseIndent(); - BuildMethods(emitContext, jsonFile.Path, sectionName, configMethod.ServiceCollectionField!, configSectionVariableName); + BuildMethods(emitContext, configMethod.ConfigurationValues, sectionName, configMethod.ServiceCollectionField!, configSectionVariableName); emitContext.DecreaseIndent(); emitContext.Write("}"); } @@ -78,10 +60,10 @@ static partial class {configClass.Name} return emitContext.ToString(); } - private void BuildMethods(EmitContext emitContext, string jsonFilePath, string sectionName, string targetExpression, string configSectionVariableName) + private void BuildMethods(EmitContext emitContext, IEnumerable> configurationValues, string sectionName, string targetExpression, string configSectionVariableName) { var configBuilder = new ConfigurationBuilder(); - configBuilder.AddJsonFile(jsonFilePath); + configBuilder.AddInMemoryCollection(configurationValues); var config = configBuilder.Build(); var directive = config.GetSection(sectionName); @@ -95,9 +77,4 @@ private void BuildMethods(EmitContext emitContext, string jsonFilePath, string s Parser.ServiceCollectionTypeName, serviceCollectionParameterName); } - - private void Diag(DiagnosticDescriptor desc, Location? location, params object?[]? messageArgs) - { - reportDiagnostic(Diagnostic.Create(desc, location, messageArgs)); - } } \ No newline at end of file diff --git a/src/ConfigurationProcessor.Gen.DependencyInjection/Parser.cs b/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Parser.cs similarity index 93% rename from src/ConfigurationProcessor.Gen.DependencyInjection/Parser.cs rename to src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Parser.cs index 521ceca..9fabff2 100644 --- a/src/ConfigurationProcessor.Gen.DependencyInjection/Parser.cs +++ b/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Parser.cs @@ -1,46 +1,46 @@ using System.Collections.Immutable; using System.Diagnostics; -using ConfigurationProcessor.Gen.DependencyInjection.Parsing; -using ConfigurationProcessor.Gen.DependencyInjection.Utility; +using ConfigurationProcessor.DependencyInjection.SourceGeneration.Parsing; +using ConfigurationProcessor.DependencyInjection.SourceGeneration.Utility; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace ConfigurationProcessor.Gen.DependencyInjection; +namespace ConfigurationProcessor.DependencyInjection.SourceGeneration; internal class Parser { internal const string DefaultConfigurationFile = "appsettings.json"; internal const string GenerateServiceRegistrationAttribute = "ConfigurationProcessor.DependencyInjection.GenerateServiceRegistrationAttribute"; internal const string ServiceCollectionTypeName = "Microsoft.Extensions.DependencyInjection.IServiceCollection"; - private readonly Compilation compilation; + private readonly GeneratorExecutionContext context; private readonly Action reportDiagnostic; private readonly CancellationToken cancellationToken; - public Parser(Compilation compilation, Action reportDiagnostic, CancellationToken cancellationToken) + public Parser(GeneratorExecutionContext context, Action reportDiagnostic, CancellationToken cancellationToken) { - this.compilation = compilation; + this.context = context; this.reportDiagnostic = reportDiagnostic; this.cancellationToken = cancellationToken; } internal IReadOnlyList GetServiceRegistrationClasses(IEnumerable classes) { - INamedTypeSymbol? generateServiceRegistrationAttribute = compilation.GetBestTypeByMetadataName(GenerateServiceRegistrationAttribute); + INamedTypeSymbol? generateServiceRegistrationAttribute = context.Compilation.GetBestTypeByMetadataName(GenerateServiceRegistrationAttribute); if (generateServiceRegistrationAttribute == null) { // nothing to do if this type isn't available return Array.Empty(); } - INamedTypeSymbol? serviceCollectionSymbol = compilation.GetBestTypeByMetadataName(ServiceCollectionTypeName); + INamedTypeSymbol? serviceCollectionSymbol = context.Compilation.GetBestTypeByMetadataName(ServiceCollectionTypeName); if (serviceCollectionSymbol == null) { // nothing to do if this type isn't available return Array.Empty(); } - INamedTypeSymbol? configurationSymbol = compilation.GetBestTypeByMetadataName("Microsoft.Extensions.Configuration.IConfiguration"); + INamedTypeSymbol? configurationSymbol = context.Compilation.GetBestTypeByMetadataName("Microsoft.Extensions.Configuration.IConfiguration"); if (configurationSymbol == null) { // nothing to do if this type isn't available @@ -55,7 +55,7 @@ internal IReadOnlyList GetServiceRegistrationClasses(I foreach (IGrouping group in classes.GroupBy(x => x.SyntaxTree)) { SyntaxTree syntaxTree = group.Key; - SemanticModel sm = compilation.GetSemanticModel(syntaxTree); + SemanticModel sm = context.Compilation.GetSemanticModel(syntaxTree); foreach (ClassDeclarationSyntax classDec in group) { @@ -170,7 +170,20 @@ internal IReadOnlyList GetServiceRegistrationClasses(I break; } - var lm = new ServiceRegistrationMethod(configurationMethodSymbol.Name, string.Join(", ", configurationMethodSymbol.Parameters.Select(ToDisplay)), method.Modifiers.ToString(), configurationFile ?? DefaultConfigurationFile, configurationSection, method.GetLocation()); + var configFile = configurationFile ?? DefaultConfigurationFile; + IDictionary configurationValues; + var jsonFile = context.AdditionalFiles.FirstOrDefault(x => Path.GetFileName(x.Path) == configFile); + if (jsonFile == null) + { + Diag(DiagnosticDescriptors.ConfigurationFileNotFound, method.GetLocation(), configFile); + continue; + } + else + { + configurationValues = JsonConfigurationFileParser.Parse(File.OpenRead(jsonFile.Path)); + } + + var lm = new ServiceRegistrationMethod(configurationMethodSymbol.Name, string.Join(", ", configurationMethodSymbol.Parameters.Select(ToDisplay)), method.Modifiers.ToString(), configurationValues, configurationSection); static string ToDisplay(IParameterSymbol parameter) { @@ -481,7 +494,7 @@ static bool IsAllowedKind(SyntaxKind kind) => static object? GetItem(TypedConstant arg) => arg.Kind == TypedConstantKind.Array ? arg.Values : arg.Value; } - if (results.Count > 0 && compilation is CSharpCompilation { LanguageVersion: LanguageVersion version and < LanguageVersion.CSharp8 }) + if (results.Count > 0 && context.Compilation is CSharpCompilation { LanguageVersion: LanguageVersion version and < LanguageVersion.CSharp8 }) { // we only support C# 8.0 and above Diag(DiagnosticDescriptors.GenerateConfigurationUnsupportedLanguageVersion, null, version.ToDisplayString(), LanguageVersion.CSharp8.ToDisplayString()); @@ -535,7 +548,7 @@ private void Diag(DiagnosticDescriptor desc, Location? location, params object?[ private bool IsBaseOrIdentity(ITypeSymbol source, ITypeSymbol dest) { - Conversion conversion = compilation.ClassifyConversion(source, dest); + Conversion conversion = context.Compilation.ClassifyConversion(source, dest); return conversion.IsIdentity || (conversion.IsReference && conversion.IsImplicit); } } \ No newline at end of file diff --git a/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Parsing/JsonConfigurationFileParser.cs b/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Parsing/JsonConfigurationFileParser.cs new file mode 100644 index 0000000..371af14 --- /dev/null +++ b/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Parsing/JsonConfigurationFileParser.cs @@ -0,0 +1,123 @@ +using System.Diagnostics; +using System.Text.Json; +using Microsoft.Extensions.Configuration; + +namespace ConfigurationProcessor.DependencyInjection.SourceGeneration.Parsing; + +/// +/// This implementation is copied from Microsoft.Extensions.Configuration.Json +/// +internal sealed class JsonConfigurationFileParser +{ + private readonly Dictionary data = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly Stack paths = new Stack(); + + private JsonConfigurationFileParser() + { + } + + public static IDictionary Parse(Stream input) + => new JsonConfigurationFileParser().ParseStream(input); + + private IDictionary ParseStream(Stream input) + { + var jsonDocumentOptions = new JsonDocumentOptions + { + CommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true, + }; + + using (var reader = new StreamReader(input)) + using (JsonDocument doc = JsonDocument.Parse(reader.ReadToEnd(), jsonDocumentOptions)) + { + if (doc.RootElement.ValueKind != JsonValueKind.Object) + { + throw new FormatException(string.Format("Invalid top level json element {0}", doc.RootElement.ValueKind)); + } + + VisitObjectElement(doc.RootElement); + } + + return data; + } + + private void VisitObjectElement(JsonElement element) + { + var isEmpty = true; + + foreach (JsonProperty property in element.EnumerateObject()) + { + isEmpty = false; + EnterContext(property.Name); + VisitValue(property.Value); + ExitContext(); + } + + SetNullIfElementIsEmpty(isEmpty); + } + + private void VisitArrayElement(JsonElement element) + { + int index = 0; + + foreach (JsonElement arrayElement in element.EnumerateArray()) + { + EnterContext(index.ToString()); + VisitValue(arrayElement); + ExitContext(); + index++; + } + + SetNullIfElementIsEmpty(isEmpty: index == 0); + } + + private void SetNullIfElementIsEmpty(bool isEmpty) + { + if (isEmpty && paths.Count > 0) + { + data[paths.Peek()] = null; + } + } + + private void VisitValue(JsonElement value) + { +#pragma warning disable SA1405 // Debug.Assert should provide message text + Debug.Assert(paths.Count > 0); +#pragma warning restore SA1405 // Debug.Assert should provide message text + + switch (value.ValueKind) + { + case JsonValueKind.Object: + VisitObjectElement(value); + break; + + case JsonValueKind.Array: + VisitArrayElement(value); + break; + + case JsonValueKind.Number: + case JsonValueKind.String: + case JsonValueKind.True: + case JsonValueKind.False: + case JsonValueKind.Null: + string key = paths.Peek(); + if (data.ContainsKey(key)) + { + throw new FormatException(string.Format("Key {0} is duplicated", key)); + } + + data[key] = value.ToString(); + break; + + default: + throw new FormatException(string.Format("Unsupported json token {0}", value.ValueKind)); + } + } + + private void EnterContext(string context) => + paths.Push(paths.Count > 0 ? + paths.Peek() + ConfigurationPath.KeyDelimiter + context : + context); + + private void ExitContext() => paths.Pop(); +} diff --git a/src/ConfigurationProcessor.Gen.DependencyInjection/Parsing/ServiceRegistrationClass.cs b/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Parsing/ServiceRegistrationClass.cs similarity index 81% rename from src/ConfigurationProcessor.Gen.DependencyInjection/Parsing/ServiceRegistrationClass.cs rename to src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Parsing/ServiceRegistrationClass.cs index f0a5101..ec1837c 100644 --- a/src/ConfigurationProcessor.Gen.DependencyInjection/Parsing/ServiceRegistrationClass.cs +++ b/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Parsing/ServiceRegistrationClass.cs @@ -1,4 +1,4 @@ -namespace ConfigurationProcessor.Gen.DependencyInjection.Parsing; +namespace ConfigurationProcessor.DependencyInjection.SourceGeneration.Parsing; internal sealed class ServiceRegistrationClass { diff --git a/src/ConfigurationProcessor.Gen.DependencyInjection/Parsing/ServiceRegistrationMethod.cs b/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Parsing/ServiceRegistrationMethod.cs similarity index 55% rename from src/ConfigurationProcessor.Gen.DependencyInjection/Parsing/ServiceRegistrationMethod.cs rename to src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Parsing/ServiceRegistrationMethod.cs index 1ff021e..743a045 100644 --- a/src/ConfigurationProcessor.Gen.DependencyInjection/Parsing/ServiceRegistrationMethod.cs +++ b/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Parsing/ServiceRegistrationMethod.cs @@ -1,8 +1,8 @@ using Microsoft.CodeAnalysis; -namespace ConfigurationProcessor.Gen.DependencyInjection.Parsing; +namespace ConfigurationProcessor.DependencyInjection.SourceGeneration.Parsing; -internal sealed record class ServiceRegistrationMethod(string Name, string Arguments, string Modifiers, string FileName, string ConfigurationSectionName, Location Location) +internal sealed record class ServiceRegistrationMethod(string Name, string Arguments, string Modifiers, IEnumerable> ConfigurationValues, string ConfigurationSectionName) { public string UniqueName { get; set; } = string.Empty; diff --git a/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Parsing/SymbolVisibility.cs b/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Parsing/SymbolVisibility.cs new file mode 100644 index 0000000..34fc9f3 --- /dev/null +++ b/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Parsing/SymbolVisibility.cs @@ -0,0 +1,8 @@ +namespace ConfigurationProcessor.DependencyInjection.SourceGeneration.Parsing; + +internal enum SymbolVisibility +{ + Public, + Private, + Internal, +} \ No newline at end of file diff --git a/src/ConfigurationProcessor.Gen.DependencyInjection/ConfigurationRegistrationGenerator.cs b/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/RegistrationGenerator.cs similarity index 80% rename from src/ConfigurationProcessor.Gen.DependencyInjection/ConfigurationRegistrationGenerator.cs rename to src/ConfigurationProcessor.DependencyInjection.SourceGeneration/RegistrationGenerator.cs index 5d2bae1..dc65f5e 100644 --- a/src/ConfigurationProcessor.Gen.DependencyInjection/ConfigurationRegistrationGenerator.cs +++ b/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/RegistrationGenerator.cs @@ -2,19 +2,21 @@ // Copyright (c) almostchristian. All rights reserved. // ------------------------------------------------------------------------------------------------- +using System.Reflection; using System.Text; -using ConfigurationProcessor.Gen.DependencyInjection.Parsing; +using ConfigurationProcessor.DependencyInjection.SourceGeneration; +using ConfigurationProcessor.DependencyInjection.SourceGeneration.Parsing; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; -namespace ConfigurationProcessor.Gen.DependencyInjection; +namespace ConfigurationProcessor; /// /// Generates method for registration based on an appsetting.json file. /// [Generator] -public class ConfigurationRegistrationGenerator : ISourceGenerator +public class RegistrationGenerator : ISourceGenerator { /// public void Initialize(GeneratorInitializationContext context) @@ -35,12 +37,17 @@ public void Execute(GeneratorExecutionContext context) System.Diagnostics.Debugger.Launch(); #endif - var p = new Parser(context.Compilation, context.ReportDiagnostic, context.CancellationToken); + var p = new Parser(context, context.ReportDiagnostic, context.CancellationToken); IReadOnlyList registrationClasses = p.GetServiceRegistrationClasses(receiver.ClassDeclarations); if (registrationClasses.Count > 0) { - var e = new Emitter(context, context.ReportDiagnostic); - string result = e.Emit(registrationClasses, context.CancellationToken); + var paths = context.Compilation.ExternalReferences.Select(x => x.Display!).ToList(); + var resolver = new PathAssemblyResolver(paths); + 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); context.AddSource("RegisterServices.g.cs", SourceText.From(result, Encoding.UTF8)); } diff --git a/src/ConfigurationProcessor.Gen.DependencyInjection/Utility/DiagnosticDescriptorHelper.cs b/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Utility/DiagnosticDescriptorHelper.cs similarity index 91% rename from src/ConfigurationProcessor.Gen.DependencyInjection/Utility/DiagnosticDescriptorHelper.cs rename to src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Utility/DiagnosticDescriptorHelper.cs index bb8d527..ae4a041 100644 --- a/src/ConfigurationProcessor.Gen.DependencyInjection/Utility/DiagnosticDescriptorHelper.cs +++ b/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Utility/DiagnosticDescriptorHelper.cs @@ -1,6 +1,6 @@ using Microsoft.CodeAnalysis; -namespace ConfigurationProcessor.Gen.DependencyInjection.Utility; +namespace ConfigurationProcessor.DependencyInjection.SourceGeneration.Utility; /// /// Helper methods for creating instances. diff --git a/src/ConfigurationProcessor.Gen.DependencyInjection/Utility/DiagnosticDescriptors.cs b/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Utility/DiagnosticDescriptors.cs similarity index 98% rename from src/ConfigurationProcessor.Gen.DependencyInjection/Utility/DiagnosticDescriptors.cs rename to src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Utility/DiagnosticDescriptors.cs index 8f369d6..e17acd7 100644 --- a/src/ConfigurationProcessor.Gen.DependencyInjection/Utility/DiagnosticDescriptors.cs +++ b/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Utility/DiagnosticDescriptors.cs @@ -1,6 +1,6 @@ using Microsoft.CodeAnalysis; -namespace ConfigurationProcessor.Gen.DependencyInjection.Utility; +namespace ConfigurationProcessor.DependencyInjection.SourceGeneration.Utility; internal static class DiagnosticDescriptors { diff --git a/src/ConfigurationProcessor.Gen.DependencyInjection/Utility/EmitContext.cs b/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Utility/EmitContext.cs similarity index 98% rename from src/ConfigurationProcessor.Gen.DependencyInjection/Utility/EmitContext.cs rename to src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Utility/EmitContext.cs index 9874a81..1cfc0ce 100644 --- a/src/ConfigurationProcessor.Gen.DependencyInjection/Utility/EmitContext.cs +++ b/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Utility/EmitContext.cs @@ -2,7 +2,7 @@ using System.Reflection; using System.Text; -namespace ConfigurationProcessor.Gen.DependencyInjection.Utility; +namespace ConfigurationProcessor.DependencyInjection.SourceGeneration.Utility; internal record class EmitContext(string Namespace, List References) { diff --git a/src/ConfigurationProcessor.Gen.DependencyInjection/Utility/Helpers.cs b/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Utility/Helpers.cs similarity index 98% rename from src/ConfigurationProcessor.Gen.DependencyInjection/Utility/Helpers.cs rename to src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Utility/Helpers.cs index e5f566c..3ac11d5 100644 --- a/src/ConfigurationProcessor.Gen.DependencyInjection/Utility/Helpers.cs +++ b/src/ConfigurationProcessor.DependencyInjection.SourceGeneration/Utility/Helpers.cs @@ -1,12 +1,10 @@ -using System.Linq; -using System.Reflection; +using System.Reflection; using ConfigurationProcessor.Core.Implementation; -using ConfigurationProcessor.Gen.DependencyInjection.Parsing; +using ConfigurationProcessor.DependencyInjection.SourceGeneration.Parsing; using Microsoft.CodeAnalysis; using Microsoft.Extensions.Configuration; -using static System.Collections.Specialized.BitVector32; -namespace ConfigurationProcessor.Gen.DependencyInjection.Utility; +namespace ConfigurationProcessor.DependencyInjection.SourceGeneration.Utility; internal static class Helpers { diff --git a/src/ConfigurationProcessor.Gen.DependencyInjection/Parsing/SymbolVisibility.cs b/src/ConfigurationProcessor.Gen.DependencyInjection/Parsing/SymbolVisibility.cs deleted file mode 100644 index 2ade24a..0000000 --- a/src/ConfigurationProcessor.Gen.DependencyInjection/Parsing/SymbolVisibility.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace ConfigurationProcessor.Gen.DependencyInjection.Parsing; - -internal enum SymbolVisibility -{ - Public, - Private, - Internal, -} \ No newline at end of file diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 46bab05..2701936 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -90,8 +90,8 @@ v1.0.0 $(NoWarn);CA1062 - - 10.0 + + 11 diff --git a/tests/ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests/ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests.csproj b/tests/ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests/ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests.csproj new file mode 100644 index 0000000..b48bdb5 --- /dev/null +++ b/tests/ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests/ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests.csproj @@ -0,0 +1,39 @@ + + + + net6.0 + enable + enable + 11 + false + + + + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/tests/ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests/EmitterTests.cs b/tests/ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests/EmitterTests.cs new file mode 100644 index 0000000..c5aa3b2 --- /dev/null +++ b/tests/ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests/EmitterTests.cs @@ -0,0 +1,101 @@ +using ConfigurationProcessor.DependencyInjection.SourceGeneration.Parsing; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.Extensions.DependencyModel; +using System.Reflection; +using System.Text; + +namespace ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests; + +public class EmitterTests +{ + [Fact] + public void AddServicesWithContextPaths() + { + TestConfig( + @"{ + ""Services"": { + ""WithChildren"": { + ""SimpleString"": ""helloworld"" + } + }, + ""ComplexObject"": true +}", + @"var sectionWithChildren = servicesSection.GetSection(""WithChildren""); +if (sectionWithChildren.Exists()) +{ + services.WithChildren(options => + { + // options.SimpleString = sectionWithChildren.GetValue(""SimpleString""); + }); +}"); + } + + private static void TestConfig(string inputJson, string expectedCsharpFragment) + { + var emitter = new Emitter(); + + var configurationValues = JsonConfigurationFileParser.Parse(new MemoryStream(Encoding.UTF8.GetBytes(inputJson))); + var rc = new ServiceRegistrationClass + { + Name = "Test", + Namespace = "TestApp", + ParentClass = null, + Keyword = "class", + }; + rc.Methods.Add(new ServiceRegistrationMethod("Register", "IServiceCollection services, IConfiguration configuration", "public partial void", configurationValues, "Services") + { + ConfigurationField = "configuration", + ServiceCollectionField = "services", + }); + + var assemblies = GetLoadedAssemblies(); + + var generatedCsharp = emitter.Emit(new[] { rc }, assemblies, default); + var expectedCsharp = @$"// + +namespace TestApp +{{ + static partial class Test + {{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute(""ConfigurationProcessor.DependencyInjection.Generator"", ""0.1.0"")] + public partial void void Register(this IServiceCollection services, IConfiguration configuration) + {{ + var servicesSection = configuration.GetSection(""Services""); + if (!servicesSection.Exists()) + {{ + return; + }} + +{IndentLines(expectedCsharpFragment, " ")} + }} + }} +}}"; + + Assert.Equal(expectedCsharp, generatedCsharp.Trim()); + } + + private static List GetLoadedAssemblies() + { + var dependencyContext = DependencyContext.Default; + + var query = from assemblyName in dependencyContext.RuntimeLibraries + .SelectMany(l => l.GetDefaultAssemblyNames(dependencyContext)).Distinct() + select assemblyName; + + return query.Select(Assembly.Load).ToList(); + } + + private static string IndentLines(string input, string indent) + { + var lines = input.Split(Environment.NewLine); + var sb = new StringBuilder(); + foreach (var line in lines) + { + sb.Append(indent); + sb.AppendLine(line); + } + + return sb.ToString().TrimEnd(); + } +} \ No newline at end of file diff --git a/tests/ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests/Usings.cs b/tests/ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests/Usings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/tests/ConfigurationProcessor.DependencyInjection.SourceGeneration.UnitTests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file