diff --git a/readme.md b/readme.md index d48346d..4a43d93 100644 --- a/readme.md +++ b/readme.md @@ -290,21 +290,6 @@ parameters), you can annotate it with `[ImportingConstructor]` from either NuGet ([System.Composition](http://nuget.org/packages/System.Composition.AttributedModel)) or .NET MEF ([System.ComponentModel.Composition](https://www.nuget.org/packages/System.ComponentModel.Composition)). -### Customize Generated Class - -You can customize the generated class namespace and name with the following -MSBuild properties: - -```xml - - MyNamespace - MyExtensions - -``` - -They default to `Microsoft.Extensions.DependencyInjection` and `AddServicesNoReflectionExtension` -respectively. - # Dogfooding diff --git a/src/DependencyInjection/DependencyInjection.csproj b/src/DependencyInjection/DependencyInjection.csproj index 0b9244d..e5b7825 100644 --- a/src/DependencyInjection/DependencyInjection.csproj +++ b/src/DependencyInjection/DependencyInjection.csproj @@ -28,7 +28,8 @@ - + + diff --git a/src/DependencyInjection/Devlooped.Extensions.DependencyInjection.props b/src/DependencyInjection/Devlooped.Extensions.DependencyInjection.props index 2fd7d51..fe31c0c 100644 --- a/src/DependencyInjection/Devlooped.Extensions.DependencyInjection.props +++ b/src/DependencyInjection/Devlooped.Extensions.DependencyInjection.props @@ -4,12 +4,10 @@ 42.42.42 true true - - - - - - + true + + true + \ No newline at end of file diff --git a/src/DependencyInjection/Devlooped.Extensions.DependencyInjection.targets b/src/DependencyInjection/Devlooped.Extensions.DependencyInjection.targets index e2489cb..b263965 100644 --- a/src/DependencyInjection/Devlooped.Extensions.DependencyInjection.targets +++ b/src/DependencyInjection/Devlooped.Extensions.DependencyInjection.targets @@ -4,8 +4,6 @@ $(IncludeServiceAttribute) - $(DefineConstants);DDI_ADDSERVICE - $(DefineConstants);DDI_ADDSERVICES @@ -13,11 +11,20 @@ - - - $(DefineConstants);DDI_ADDSERVICE - $(DefineConstants);DDI_ADDSERVICES - - - + + + + + + + + + + \ No newline at end of file diff --git a/src/DependencyInjection/IncrementalGenerator.cs b/src/DependencyInjection/IncrementalGenerator.cs index b0082f3..dd9ee2d 100644 --- a/src/DependencyInjection/IncrementalGenerator.cs +++ b/src/DependencyInjection/IncrementalGenerator.cs @@ -51,14 +51,44 @@ record ServiceRegistration(int Lifetime, TypeSyntax? AssignableTo, string? FullN public void Initialize(IncrementalGeneratorInitializationContext context) { - var types = context.CompilationProvider.SelectMany((x, c) => + var compilation = context.CompilationProvider.Select((compilation, _) => { - var visitor = new TypesVisitor(s => x.IsSymbolAccessible(s), c); - x.GlobalNamespace.Accept(visitor); + // Add missing types as needed since we depend on the static generator potentially and can't + // rely on its sources being added. + var parse = (CSharpParseOptions)compilation.SyntaxTrees.FirstOrDefault().Options; + + if (compilation.GetTypeByMetadataName("Microsoft.Extensions.DependencyInjection.AddServicesNoReflectionExtension") is null) + { + compilation = compilation.AddSyntaxTrees( + CSharpSyntaxTree.ParseText(ThisAssembly.Resources.AddServicesNoReflectionExtension.Text, parse)); + } + + if (compilation.GetTypeByMetadataName("Microsoft.Extensions.DependencyInjection.ServiceAttribute") is null) + { + compilation = compilation.AddSyntaxTrees( + CSharpSyntaxTree.ParseText(ThisAssembly.Resources.ServiceAttribute.Text, parse), + CSharpSyntaxTree.ParseText(ThisAssembly.Resources.ServiceAttribute_1.Text, parse)); + } + + return compilation; + }); + + var types = compilation.Combine(context.AnalyzerConfigOptionsProvider).SelectMany((x, c) => + { + (var compilation, var options) = x; + + // We won't add any registrations in this case. + if (!options.GlobalOptions.TryGetValue("build_property.AddServicesExtension", out var value) || + !bool.TryParse(value, out var addServices) || !addServices) + return Enumerable.Empty(); + + var visitor = new TypesVisitor(s => compilation.IsSymbolAccessible(s), c); + compilation.GlobalNamespace.Accept(visitor); + // Also visit aliased references, which will not become part of the global:: namespace - foreach (var symbol in x.References + foreach (var symbol in compilation.References .Where(r => !r.Properties.Aliases.IsDefaultOrEmpty) - .Select(r => x.GetAssemblyOrModuleSymbol(r))) + .Select(r => compilation.GetAssemblyOrModuleSymbol(r))) { symbol?.Accept(visitor); } @@ -152,8 +182,6 @@ bool IsExport(AttributeData attr) }) .Where(x => x != null); - var options = context.AnalyzerConfigOptionsProvider.Combine(context.CompilationProvider); - // Only requisite is that we define Scoped = 0, Singleton = 1 and Transient = 2. // This matches https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicelifetime?view=dotnet-plat-ext-6.0#fields @@ -164,11 +192,18 @@ bool IsExport(AttributeData attr) .CreateSyntaxProvider( predicate: static (node, _) => node is InvocationExpressionSyntax invocation && invocation.ArgumentList.Arguments.Count != 0 && GetInvokedMethodName(invocation) == nameof(AddServicesNoReflectionExtension.AddServices), transform: static (ctx, _) => GetServiceRegistration((InvocationExpressionSyntax)ctx.Node, ctx.SemanticModel)) - .Where(details => details != null) + .Combine(context.AnalyzerConfigOptionsProvider) + .Where(x => + { + (var registration, var options) = x; + return options.GlobalOptions.TryGetValue("build_property.AddServicesExtension", out var value) && + bool.TryParse(value, out var addServices) && addServices && registration is not null; + }) + .Select((x, _) => x.Left) .Collect(); // Project matching service types to register with the given lifetime. - var conventionServices = types.Combine(methodInvocations.Combine(context.CompilationProvider)).SelectMany((pair, cancellationToken) => + var conventionServices = types.Combine(methodInvocations.Combine(compilation)).SelectMany((pair, cancellationToken) => { var (typeSymbol, (registrations, compilation)) = pair; var results = ImmutableArray.CreateBuilder(); @@ -196,33 +231,33 @@ bool IsExport(AttributeData attr) .SelectMany((tuple, _) => ImmutableArray.CreateRange([tuple.Item1, tuple.Item2])) .SelectMany((items, _) => items.Distinct().ToImmutableArray()); - RegisterServicesOutput(context, finalServices, options); + RegisterServicesOutput(context, finalServices, compilation); } - void RegisterServicesOutput(IncrementalGeneratorInitializationContext context, IncrementalValuesProvider services, IncrementalValueProvider<(AnalyzerConfigOptionsProvider Left, Compilation Right)> options) + void RegisterServicesOutput(IncrementalGeneratorInitializationContext context, IncrementalValuesProvider services, IncrementalValueProvider compilation) { context.RegisterImplementationSourceOutput( - services.Where(x => x!.Lifetime == 0 && x.Key is null).Select((x, _) => new KeyedService(x!.Type, null)).Collect().Combine(options), + services.Where(x => x!.Lifetime == 0 && x.Key is null).Select((x, _) => new KeyedService(x!.Type, null)).Collect().Combine(compilation), (ctx, data) => AddPartial("AddSingleton", ctx, data)); context.RegisterImplementationSourceOutput( - services.Where(x => x!.Lifetime == 1 && x.Key is null).Select((x, _) => new KeyedService(x!.Type, null)).Collect().Combine(options), + services.Where(x => x!.Lifetime == 1 && x.Key is null).Select((x, _) => new KeyedService(x!.Type, null)).Collect().Combine(compilation), (ctx, data) => AddPartial("AddScoped", ctx, data)); context.RegisterImplementationSourceOutput( - services.Where(x => x!.Lifetime == 2 && x.Key is null).Select((x, _) => new KeyedService(x!.Type, null)).Collect().Combine(options), + services.Where(x => x!.Lifetime == 2 && x.Key is null).Select((x, _) => new KeyedService(x!.Type, null)).Collect().Combine(compilation), (ctx, data) => AddPartial("AddTransient", ctx, data)); context.RegisterImplementationSourceOutput( - services.Where(x => x!.Lifetime == 0 && x.Key is not null).Select((x, _) => new KeyedService(x!.Type, x.Key!)).Collect().Combine(options), + services.Where(x => x!.Lifetime == 0 && x.Key is not null).Select((x, _) => new KeyedService(x!.Type, x.Key!)).Collect().Combine(compilation), (ctx, data) => AddPartial("AddKeyedSingleton", ctx, data)); context.RegisterImplementationSourceOutput( - services.Where(x => x!.Lifetime == 1 && x.Key is not null).Select((x, _) => new KeyedService(x!.Type, x.Key!)).Collect().Combine(options), + services.Where(x => x!.Lifetime == 1 && x.Key is not null).Select((x, _) => new KeyedService(x!.Type, x.Key!)).Collect().Combine(compilation), (ctx, data) => AddPartial("AddKeyedScoped", ctx, data)); context.RegisterImplementationSourceOutput( - services.Where(x => x!.Lifetime == 2 && x.Key is not null).Select((x, _) => new KeyedService(x!.Type, x.Key!)).Collect().Combine(options), + services.Where(x => x!.Lifetime == 2 && x.Key is not null).Select((x, _) => new KeyedService(x!.Type, x.Key!)).Collect().Combine(compilation), (ctx, data) => AddPartial("AddKeyedTransient", ctx, data)); } @@ -240,13 +275,22 @@ void RegisterServicesOutput(IncrementalGeneratorInitializationContext context, I var options = (CSharpParseOptions)invocation.SyntaxTree.Options; - // NOTE: we need to add the sources that *another* generator emits (the static files) - // because otherwise all invocations will basically have no semantic info since it wasn't there - // when the source generations invocations started. - var compilation = semanticModel.Compilation.AddSyntaxTrees( - CSharpSyntaxTree.ParseText(ThisAssembly.Resources.ServiceAttribute.Text, options), - CSharpSyntaxTree.ParseText(ThisAssembly.Resources.ServiceAttribute_1.Text, options), - CSharpSyntaxTree.ParseText(ThisAssembly.Resources.AddServicesNoReflectionExtension.Text, options)); + var compilation = semanticModel.Compilation; + + // Add missing types as needed since we depend on the static generator potentially and can't + // rely on its sources being added. + if (compilation.GetTypeByMetadataName("Microsoft.Extensions.DependencyInjection.AddServicesNoReflectionExtension") is null) + { + compilation = compilation.AddSyntaxTrees( + CSharpSyntaxTree.ParseText(ThisAssembly.Resources.AddServicesNoReflectionExtension.Text, options)); + } + + if (compilation.GetTypeByMetadataName("Microsoft.Extensions.DependencyInjection.ServiceAttribute") is null) + { + compilation = compilation.AddSyntaxTrees( + CSharpSyntaxTree.ParseText(ThisAssembly.Resources.ServiceAttribute.Text, options), + CSharpSyntaxTree.ParseText(ThisAssembly.Resources.ServiceAttribute_1.Text, options)); + } var model = compilation.GetSemanticModel(invocation.SyntaxTree); @@ -292,46 +336,37 @@ void RegisterServicesOutput(IncrementalGeneratorInitializationContext context, I return null; } - void AddPartial(string methodName, SourceProductionContext ctx, (ImmutableArray Types, (AnalyzerConfigOptionsProvider Config, Compilation Compilation) Options) data) + void AddPartial(string methodName, SourceProductionContext ctx, (ImmutableArray Types, Compilation Compilation) data) { var builder = new StringBuilder() .AppendLine("// "); - var rootNs = data.Options.Config.GlobalOptions.TryGetValue("build_property.AddServicesNamespace", out var value) && !string.IsNullOrEmpty(value) - ? value - : "Microsoft.Extensions.DependencyInjection"; - - var className = data.Options.Config.GlobalOptions.TryGetValue("build_property.AddServicesClassName", out value) && !string.IsNullOrEmpty(value) ? - value : "AddServicesNoReflectionExtension"; - - foreach (var alias in data.Options.Compilation.References.SelectMany(r => r.Properties.Aliases)) + foreach (var alias in data.Compilation.References.SelectMany(r => r.Properties.Aliases)) { builder.AppendLine($"extern alias {alias};"); } builder.AppendLine( $$""" - #if DDI_ADDSERVICES using Microsoft.Extensions.DependencyInjection.Extensions; using System; - namespace {{rootNs}} + namespace Microsoft.Extensions.DependencyInjection { - static partial class {{className}} + static partial class AddServicesNoReflectionExtension { static partial void {{methodName}}Services(IServiceCollection services) { """); - AddServices(data.Types.Where(x => x.Key is null).Select(x => x.Type), data.Options.Compilation, methodName, builder); - AddKeyedServices(data.Types.Where(x => x.Key is not null), data.Options.Compilation, methodName, builder); + AddServices(data.Types.Where(x => x.Key is null).Select(x => x.Type), data.Compilation, methodName, builder); + AddKeyedServices(data.Types.Where(x => x.Key is not null), data.Compilation, methodName, builder); builder.AppendLine( """ } } } - #endif """); ctx.AddSource(methodName + ".g", builder.ToString().Replace("\r\n", "\n").Replace("\n", Environment.NewLine)); diff --git a/src/DependencyInjection/StaticGenerator.cs b/src/DependencyInjection/StaticGenerator.cs index 1c816a0..0c2f10c 100644 --- a/src/DependencyInjection/StaticGenerator.cs +++ b/src/DependencyInjection/StaticGenerator.cs @@ -4,7 +4,6 @@ using System.Text; using Devlooped.Sponsors; using Microsoft.CodeAnalysis; -using Microsoft.Extensions.DependencyInjection; using static Devlooped.Sponsors.SponsorLink; namespace Devlooped.Extensions.DependencyInjection; @@ -12,110 +11,103 @@ namespace Devlooped.Extensions.DependencyInjection; [Generator(LanguageNames.CSharp)] public class StaticGenerator : ISourceGenerator { - public const string DefaultNamespace = "Microsoft.Extensions.DependencyInjection"; - public const string DefaultAddServicesClass = nameof(AddServicesNoReflectionExtension); - public static string AddServicesExtension => ThisAssembly.Resources.AddServicesNoReflectionExtension.Text; public static string ServiceAttribute => ThisAssembly.Resources.ServiceAttribute.Text; public static string ServiceAttributeT => ThisAssembly.Resources.ServiceAttribute_1.Text; - public void Initialize(GeneratorInitializationContext context) - { - context.RegisterForPostInitialization(c => - { - c.AddSource("ServiceAttribute.g", ThisAssembly.Resources.ServiceAttribute.Text); - c.AddSource("ServiceAttribute`1.g", ThisAssembly.Resources.ServiceAttribute_1.Text); - }); - } + public void Initialize(GeneratorInitializationContext context) { } public void Execute(GeneratorExecutionContext context) { - var rootNs = context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.AddServicesNamespace", out var value) && !string.IsNullOrEmpty(value) - ? value : DefaultNamespace; + // Non-editor source is added by the MSBuild targets + if (!IsEditor) + return; - var className = context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.AddServicesClassName", out value) && !string.IsNullOrEmpty(value) ? - value : DefaultAddServicesClass; + var code = ThisAssembly.Resources.AddServicesNoReflectionExtension.Text; + var status = Diagnostics.GetOrSetStatus(context.GetStatusOptions()); + string? remarks = default; + string? warn = default; - var code = ThisAssembly.Resources.AddServicesNoReflectionExtension.Text - .Replace("namespace " + DefaultNamespace, "namespace " + rootNs) - .Replace(DefaultAddServicesClass, className); - - if (IsEditor) + if (status == SponsorStatus.Unknown || status == SponsorStatus.Expired) { - var status = Diagnostics.GetOrSetStatus(context.GetStatusOptions()); - string? remarks = default; - string? warn = default; + warn = + $""" + [Obsolete("{string.Format(CultureInfo.CurrentCulture, Resources.Editor_Disabled, Funding.Product, Funding.HelpUrl)}", false + #if NET6_0_OR_GREATER + , UrlFormat = "{Funding.HelpUrl}" + #endif + )] + """; - if (status == SponsorStatus.Unknown || status == SponsorStatus.Expired) - { - warn = - $""" - [Obsolete("{string.Format(CultureInfo.CurrentCulture, Resources.Editor_Disabled, Funding.Product, Funding.HelpUrl)}", false - #if NET6_0_OR_GREATER - , UrlFormat = "{Funding.HelpUrl}" - #endif - )] - """; + remarks = Resources.Editor_DisabledRemarks; + } + else if (status == SponsorStatus.Grace && Diagnostics.TryGet() is { } grace && grace.Properties.TryGetValue(nameof(SponsorStatus.Grace), out var days)) + { + remarks = string.Format(CultureInfo.CurrentCulture, Resources.Editor_GraceRemarks, days); + } - remarks = Resources.Editor_DisabledRemarks; - } - else if (status == SponsorStatus.Grace && Diagnostics.TryGet() is { } grace && grace.Properties.TryGetValue(nameof(SponsorStatus.Grace), out var days)) + if (remarks != null) + { + // Remove /// and /// LINES from the remarks string + var builder = new StringBuilder(); + foreach (var line in ReadLines(remarks)) { - remarks = string.Format(CultureInfo.CurrentCulture, Resources.Editor_GraceRemarks, days); - } - - if (remarks != null) - { - // Remove /// and /// LINES from the remarks string - var builder = new StringBuilder(); - foreach (var line in ReadLines(remarks)) - { - if (line.EndsWith("/// ") || line.EndsWith("/// ")) - continue; - if (line.TrimStart() is { Length: > 0 } trimmed && trimmed.StartsWith("///")) - builder.AppendLine(trimmed); - } - remarks = builder.AppendLine("///").ToString(); + if (line.EndsWith("/// ") || line.EndsWith("/// ")) + continue; + if (line.TrimStart() is { Length: > 0 } trimmed && trimmed.StartsWith("///")) + builder.AppendLine(trimmed); } + remarks = builder.AppendLine("///").ToString(); + } - if (remarks != null || warn != null) + if (remarks != null || warn != null) + { + var builder = new StringBuilder(); + foreach (var line in ReadLines(code)) { - var builder = new StringBuilder(); - foreach (var line in ReadLines(code)) + if (remarks != null && line.EndsWith("/// ")) { - if (remarks != null && line.EndsWith("/// ")) + builder.AppendLine(line); + // trim the remarks line to remove leading spaces and + // replace them with the indenting from the target code line + var indent = line.IndexOf("/// "); + foreach (var rline in ReadLines(remarks)) { - builder.AppendLine(line); - // trim the remarks line to remove leading spaces and - // replace them with the indenting from the target code line - var indent = line.IndexOf("/// "); - foreach (var rline in ReadLines(remarks)) - { - builder.Append(new string(' ', indent)).AppendLine(rline); - } + builder.Append(new string(' ', indent)).AppendLine(rline); } - else if (warn != null && line.EndsWith("[DDIAddServices]")) - { - builder.AppendLine(line); - // trim the remarks line to remove leading spaces and - // replace them with the indenting from the target code line - var indent = line.IndexOf("[DDIAddServices]"); - // append indentation and the warning, also splitting lines and trimming start - foreach (var wline in ReadLines(warn)) - { - builder.Append(new string(' ', indent)).AppendLine(wline.TrimStart()); - } - } - else + } + else if (warn != null && line.EndsWith("[DDIAddServices]")) + { + builder.AppendLine(line); + // trim the remarks line to remove leading spaces and + // replace them with the indenting from the target code line + var indent = line.IndexOf("[DDIAddServices]"); + // append indentation and the warning, also splitting lines and trimming start + foreach (var wline in ReadLines(warn)) { - builder.AppendLine(line); + builder.Append(new string(' ', indent)).AppendLine(wline.TrimStart()); } } - code = builder.ToString(); + else + { + builder.AppendLine(line); + } } + code = builder.ToString(); } - context.AddSource(DefaultAddServicesClass + ".g", code); + if (context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.AddServicesExtension", out var value) && + bool.TryParse(value, out var addServices) && addServices) + { + context.AddSource(nameof(ThisAssembly.Resources.AddServicesNoReflectionExtension) + ".g", code); + } + + if (context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.AddServiceAttribute", out value) && + bool.TryParse(value, out var addServiceAttribute) && addServiceAttribute) + { + context.AddSource("ServiceAttribute.g", ThisAssembly.Resources.ServiceAttribute.Text); + context.AddSource("ServiceAttribute`1.g", ThisAssembly.Resources.ServiceAttribute_1.Text); + } } static IEnumerable ReadLines(string text) diff --git a/src/DependencyInjection/AddServicesNoReflectionExtension.cs b/src/DependencyInjection/compile/AddServicesNoReflectionExtension.cs similarity index 98% rename from src/DependencyInjection/AddServicesNoReflectionExtension.cs rename to src/DependencyInjection/compile/AddServicesNoReflectionExtension.cs index a9b00a8..d3141cb 100644 --- a/src/DependencyInjection/AddServicesNoReflectionExtension.cs +++ b/src/DependencyInjection/compile/AddServicesNoReflectionExtension.cs @@ -1,8 +1,6 @@ // -#if DDI_ADDSERVICES using System; using System.ComponentModel; -using Microsoft.Extensions.DependencyInjection; namespace Microsoft.Extensions.DependencyInjection { @@ -124,5 +122,4 @@ public static IServiceCollection AddServices(this IServiceCollection services) [AttributeUsage(AttributeTargets.Method)] class DDIAddServicesAttribute : Attribute { } } -} -#endif \ No newline at end of file +} \ No newline at end of file diff --git a/src/DependencyInjection/ServiceAttribute.cs b/src/DependencyInjection/compile/ServiceAttribute.cs similarity index 96% rename from src/DependencyInjection/ServiceAttribute.cs rename to src/DependencyInjection/compile/ServiceAttribute.cs index 0a526d4..96cc264 100644 --- a/src/DependencyInjection/ServiceAttribute.cs +++ b/src/DependencyInjection/compile/ServiceAttribute.cs @@ -1,6 +1,5 @@ // #nullable enable -#if DDI_ADDSERVICE using System; namespace Microsoft.Extensions.DependencyInjection @@ -21,5 +20,4 @@ public ServiceAttribute(ServiceLifetime lifetime = ServiceLifetime.Singleton) { /// public ServiceAttribute(object key, ServiceLifetime lifetime = ServiceLifetime.Singleton) { } } -} -#endif \ No newline at end of file +} \ No newline at end of file diff --git a/src/DependencyInjection/ServiceAttribute`1.cs b/src/DependencyInjection/compile/ServiceAttribute`1.cs similarity index 96% rename from src/DependencyInjection/ServiceAttribute`1.cs rename to src/DependencyInjection/compile/ServiceAttribute`1.cs index 4de59fb..b296b48 100644 --- a/src/DependencyInjection/ServiceAttribute`1.cs +++ b/src/DependencyInjection/compile/ServiceAttribute`1.cs @@ -1,5 +1,4 @@ // -#if DDI_ADDSERVICE using System; namespace Microsoft.Extensions.DependencyInjection @@ -18,5 +17,4 @@ partial class ServiceAttribute : Attribute /// public ServiceAttribute(TKey key, ServiceLifetime lifetime = ServiceLifetime.Singleton) { } } -} -#endif \ No newline at end of file +} \ No newline at end of file