From 2a4c55686cbebf3099792bc9c50741bec3ef4ce0 Mon Sep 17 00:00:00 2001 From: neuecc Date: Mon, 16 Dec 2024 08:23:00 +0900 Subject: [PATCH] generate Config --- ReadMe.md | 2 +- sandbox/GeneratorSandbox/Program.cs | 107 ++- src/ConsoleAppFramework/ConsoleAppBaseCode.cs | 528 +++++++++++++++ .../ConsoleAppFramework.csproj | 6 + .../ConsoleAppFrameworkGeneratorOptions.cs | 3 - .../ConsoleAppGenerator.cs | 612 +++--------------- src/ConsoleAppFramework/Emitter.cs | 180 +++++- .../SourceGeneratorContexts.cs | 5 + .../GeneratorOptionsTest.cs | 64 ++ 9 files changed, 947 insertions(+), 560 deletions(-) create mode 100644 src/ConsoleAppFramework/ConsoleAppBaseCode.cs delete mode 100644 src/ConsoleAppFramework/ConsoleAppFrameworkGeneratorOptions.cs create mode 100644 src/ConsoleAppFramework/SourceGeneratorContexts.cs create mode 100644 tests/ConsoleAppFramework.GeneratorTests/GeneratorOptionsTest.cs diff --git a/ReadMe.md b/ReadMe.md index 6f9d784..433b7eb 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -369,7 +369,7 @@ app.Add("foo baz", async (string foo = "test", CancellationToken cancellationTok The Source Generator generates four fields and holds them with specific types. ```csharp -partial struct ConsoleAppBuilder +partial class ConsoleAppBuilder { Action command0 = default!; Action command1 = default!; diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index cf24093..a557efd 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -1,14 +1,34 @@ -using ConsoleAppFramework; +#nullable enable + +using ConsoleAppFramework; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System.Reflection; +using ZLogger; [assembly: ConsoleAppFrameworkGeneratorOptions(DisableNamingConversion = true)] -args = ["HelloWorld", "--help"]; -var app = ConsoleApp.Create(); -//app.Add(); -app.Add(); -app.Run(args); +var app = ConsoleApp.Create() + ; + +app.ConfigureDefaultConfiguration(); + +app.ConfigureServices(services => +{ + +}); + + // .ConfigureLogging( + // .ConfigureDefaultConfiguration() + // ; + +app.Add("", () => { }); + +app.Run(args); @@ -71,22 +91,79 @@ internal static partial class ConsoleApp - //internal partial struct ConsoleAppBuilder + //internal partial class ConsoleAppBuilder //{ - // /// - // /// Add all [RegisterCommands] types as ConsoleApp command. - // /// - // public void RegisterAll() + // bool requireConfiguration; + // IConfiguration? configuration; + // Action? configureServices; + // Action? configureLogging; + + // /// Create configuration with SetBasePath(Directory.GetCurrentDirectory()) and AddJsonFile("appsettings.json"). + // public void ConfigureDefaultConfiguration(Action configure) // { + // var config = new ConfigurationBuilder(); + // config.SetBasePath(System.IO.Directory.GetCurrentDirectory()); + // config.AddJsonFile("appsettings.json", optional: true); + // configure(config); + // configuration = config.Build(); // } - // /// - // /// Add all [RegisterCommands] types as ConsoleApp command. - // /// - // public void RegisterAll(string commandPath) + // public void ConfigureEmptyConfiguration(Action configure) // { + // var config = new ConfigurationBuilder(); + // configure(config); + // configuration = config.Build(); // } + // public void ConfigureServices(Action configure) + // { + // this.configureServices = (_, services) => configure(services); + // } + + // public void ConfigureServices(Action configure) + // { + // this.requireConfiguration = true; + // this.configureServices = configure; + // } + + // public void ConfigureLogging(Action configure) + // { + // this.configureLogging = (_, builder) => configure(builder); + // } + + // public void ConfigureLogging(Action configure) + // { + // this.requireConfiguration = true; + // this.configureLogging = configure; + // } + + // public void BuildAndSetServiceProvider() + // { + // if (configureServices == null && configureLogging == null) return; + + // if (configureServices != null) + // { + // var services = new ServiceCollection(); + // configureServices?.Invoke(configuration!, services); + + // if (configureLogging != null) + // { + // var config = configuration; + // if (requireConfiguration && config == null) + // { + // config = new ConfigurationRoot(Array.Empty()); + // } + + // var configure = configureLogging; + // services.AddLogging(logging => + // { + // configure!(config!, logging); + // }); + // } + + // ConsoleApp.ServiceProvider = services.BuildServiceProvider(); + // } + // } //} } diff --git a/src/ConsoleAppFramework/ConsoleAppBaseCode.cs b/src/ConsoleAppFramework/ConsoleAppBaseCode.cs new file mode 100644 index 0000000..2134093 --- /dev/null +++ b/src/ConsoleAppFramework/ConsoleAppBaseCode.cs @@ -0,0 +1,528 @@ +namespace ConsoleAppFramework; + +public static class ConsoleAppBaseCode +{ + public const string GeneratedCodeHeader = """ +// +#nullable enable +#pragma warning disable CS0108 // hides inherited member +#pragma warning disable CS0162 // Unreachable code +#pragma warning disable CS0164 // This label has not been referenced +#pragma warning disable CS0219 // Variable assigned but never used +#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. +#pragma warning disable CS8601 // Possible null reference assignment +#pragma warning disable CS8602 +#pragma warning disable CS8604 // Possible null reference argument for parameter +#pragma warning disable CS8619 +#pragma warning disable CS8620 +#pragma warning disable CS8631 // The type cannot be used as type parameter in the generic type or method +#pragma warning disable CS8765 // Nullability of type of parameter +#pragma warning disable CS9074 // The 'scoped' modifier of parameter doesn't match overridden or implemented member +#pragma warning disable CA1050 // Declare types in namespaces. +#pragma warning disable CS1998 +#pragma warning disable CS8625 + +namespace ConsoleAppFramework; + +using System; +using System.Text; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; +using System.Diagnostics.CodeAnalysis; +using System.ComponentModel.DataAnnotations; + +"""; + + public const string InitializationCode = """ +// +#nullable enable +#pragma warning disable CS0108 // hides inherited member +#pragma warning disable CS0162 // Unreachable code +#pragma warning disable CS0164 // This label has not been referenced +#pragma warning disable CS0219 // Variable assigned but never used +#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. +#pragma warning disable CS8601 // Possible null reference assignment +#pragma warning disable CS8602 +#pragma warning disable CS8604 // Possible null reference argument for parameter +#pragma warning disable CS8619 +#pragma warning disable CS8620 +#pragma warning disable CS8631 // The type cannot be used as type parameter in the generic type or method +#pragma warning disable CS8765 // Nullability of type of parameter +#pragma warning disable CS9074 // The 'scoped' modifier of parameter doesn't match overridden or implemented member +#pragma warning disable CA1050 // Declare types in namespaces. +#pragma warning disable CS1998 +#pragma warning disable CS8625 + +namespace ConsoleAppFramework; + +using System; +using System.Text; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; +using System.Diagnostics.CodeAnalysis; +using System.ComponentModel.DataAnnotations; + +#if !USE_EXTERNAL_CONSOLEAPP_ABSTRACTIONS + +internal interface IArgumentParser +{ + static abstract bool TryParse(ReadOnlySpan s, out T result); +} + +internal record class ConsoleAppContext(string CommandName, string[] Arguments, object? State); + +internal abstract class ConsoleAppFilter(ConsoleAppFilter next) +{ + protected readonly ConsoleAppFilter Next = next; + + public abstract Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken); +} + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = false)] +internal sealed class ConsoleAppFilterAttribute : Attribute + where T : ConsoleAppFilter +{ +} + +internal sealed class ArgumentParseFailedException(string message) : Exception(message) +{ +} + +#endif + +[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] +internal sealed class FromServicesAttribute : Attribute +{ +} + +[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] +internal sealed class ArgumentAttribute : Attribute +{ +} + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] +internal sealed class CommandAttribute : Attribute +{ + public string Command { get; } + + public CommandAttribute(string command) + { + this.Command = command; + } +} + +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +internal sealed class RegisterCommandsAttribute : Attribute +{ + public string CommandPath { get; } + + public RegisterCommandsAttribute() + { + this.CommandPath = ""; + } + + public RegisterCommandsAttribute(string commandPath) + { + this.CommandPath = commandPath; + } +} + +[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false, Inherited = false)] +public class ConsoleAppFrameworkGeneratorOptionsAttribute : Attribute +{ + public bool DisableNamingConversion { get; set; } +} + +internal static partial class ConsoleApp +{ + public static IServiceProvider? ServiceProvider { get; set; } + public static TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(5); + public static System.Text.Json.JsonSerializerOptions? JsonSerializerOptions { get; set; } + public static string? Version { get; set; } + + static Action? logAction; + public static Action Log + { + get => logAction ??= Console.WriteLine; + set => logAction = value; + } + + static Action? logErrorAction; + public static Action LogError + { + get => logErrorAction ??= (static msg => Log(msg)); + set => logErrorAction = value; + } + + /// + /// You can pass second argument that generates new Run overload. + /// ConsoleApp.Run(args, (int x, int y) => { });
+ /// ConsoleApp.Run(args, Foo);
+ /// ConsoleApp.Run(args, &Foo);
+ ///
+ public static void Run(string[] args) + { + } + + /// + /// You can pass second argument that generates new RunAsync overload. + /// ConsoleApp.RunAsync(args, (int x, int y) => { });
+ /// ConsoleApp.RunAsync(args, Foo);
+ /// ConsoleApp.RunAsync(args, &Foo);
+ ///
+ public static Task RunAsync(string[] args) + { + return Task.CompletedTask; + } + + public static ConsoleAppBuilder Create() => new ConsoleAppBuilder(); + + static void ThrowArgumentParseFailed(string argumentName, string value) + { + throw new ArgumentParseFailedException($"Argument '{argumentName}' failed to parse, provided value: {value}"); + } + + static void ThrowRequiredArgumentNotParsed(string name) + { + throw new ArgumentParseFailedException($"Required argument '{name}' was not specified."); + } + + static void ThrowArgumentNameNotFound(string argumentName) + { + throw new ArgumentParseFailedException($"Argument '{argumentName}' is not recognized."); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static bool TryIncrementIndex(ref int index, int length) + { + if ((index + 1) < length) + { + index += 1; + return true; + } + return false; + } + + static bool TryParseParamsArray(ReadOnlySpan args, ref T[] result, ref int i) + where T : IParsable + { + result = new T[args.Length - i]; + var resultIndex = 0; + for (; i < args.Length; i++) + { + if (!T.TryParse(args[i], null, out result[resultIndex++]!)) return false; + } + return true; + } + + static bool TrySplitParse(ReadOnlySpan s, out T[] result) + where T : ISpanParsable + { + if (s.StartsWith("[")) + { + try + { + result = System.Text.Json.JsonSerializer.Deserialize(s, JsonSerializerOptions)!; + return true; + } + catch + { + result = default!; + return false; + } + } + + var count = s.Count(',') + 1; + result = new T[count]; + + var source = s; + var destination = result.AsSpan(); + Span ranges = stackalloc Range[Math.Min(count, 128)]; + + while (true) + { + var splitCount = source.Split(ranges, ','); + var parseTo = splitCount; + if (splitCount == 128 && source[ranges[^1]].Contains(',')) + { + parseTo = splitCount - 1; + } + + for (int i = 0; i < parseTo; i++) + { + if (!T.TryParse(source[ranges[i]], null, out destination[i]!)) + { + return false; + } + } + destination = destination.Slice(parseTo); + + if (destination.Length != 0) + { + source = source[ranges[^1]]; + continue; + } + else + { + break; + } + } + + return true; + } + + static void ValidateParameter(object? value, ParameterInfo parameter, ValidationContext validationContext, ref StringBuilder? errorMessages) + { + validationContext.DisplayName = parameter.Name ?? ""; + validationContext.Items.Clear(); + + foreach (var validator in parameter.GetCustomAttributes(false)) + { + var result = validator.GetValidationResult(value, validationContext); + if (result != null) + { + if (errorMessages == null) + { + errorMessages = new StringBuilder(); + } + errorMessages.AppendLine(result.ErrorMessage); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static bool TryShowHelpOrVersion(ReadOnlySpan args, int requiredParameterCount, int helpId) + { + if (args.Length == 0) + { + if (requiredParameterCount == 0) return false; + + ShowHelp(helpId); + return true; + } + + if (args.Length == 1) + { + switch (args[0]) + { + case "--version": + ShowVersion(); + return true; + case "-h": + case "--help": + ShowHelp(helpId); + return true; + default: + break; + } + } + + return false; + } + + static void ShowVersion() + { + if (Version != null) + { + Log(Version); + return; + } + + var asm = Assembly.GetEntryAssembly(); + var version = "1.0.0"; + var infoVersion = asm!.GetCustomAttribute(); + if (infoVersion != null) + { + version = infoVersion.InformationalVersion; + var i = version.IndexOf('+'); + if (i != -1) + { + version = version.Substring(0, i); + } + } + else + { + var asmVersion = asm!.GetCustomAttribute(); + if (asmVersion != null) + { + version = asmVersion.Version; + } + } + Log(version); + } + + static partial void ShowHelp(int helpId); + + static async Task RunWithFilterAsync(string commandName, string[] args, ConsoleAppFilter invoker) + { + using var posixSignalHandler = PosixSignalHandler.Register(Timeout); + try + { + await Task.Run(() => invoker.InvokeAsync(new ConsoleAppContext(commandName, args, null), posixSignalHandler.Token)).WaitAsync(posixSignalHandler.TimeoutToken); + } + catch (Exception ex) + { + if (ex is OperationCanceledException) + { + Environment.ExitCode = 130; + return; + } + + Environment.ExitCode = 1; + if (ex is ValidationException or ArgumentParseFailedException) + { + LogError(ex.Message); + } + else + { + LogError(ex.ToString()); + } + } + } + + sealed class PosixSignalHandler : IDisposable + { + public CancellationToken Token => cancellationTokenSource.Token; + public CancellationToken TimeoutToken => timeoutCancellationTokenSource.Token; + + CancellationTokenSource cancellationTokenSource; + CancellationTokenSource timeoutCancellationTokenSource; + TimeSpan timeout; + + PosixSignalRegistration? sigInt; + PosixSignalRegistration? sigQuit; + PosixSignalRegistration? sigTerm; + + PosixSignalHandler(TimeSpan timeout) + { + this.cancellationTokenSource = new CancellationTokenSource(); + this.timeoutCancellationTokenSource = new CancellationTokenSource(); + this.timeout = timeout; + } + + public static PosixSignalHandler Register(TimeSpan timeout) + { + var handler = new PosixSignalHandler(timeout); + + Action handleSignal = handler.HandlePosixSignal; + + handler.sigInt = PosixSignalRegistration.Create(PosixSignal.SIGINT, handleSignal); + handler.sigQuit = PosixSignalRegistration.Create(PosixSignal.SIGQUIT, handleSignal); + handler.sigTerm = PosixSignalRegistration.Create(PosixSignal.SIGTERM, handleSignal); + + return handler; + } + + void HandlePosixSignal(PosixSignalContext context) + { + context.Cancel = true; + cancellationTokenSource.Cancel(); + timeoutCancellationTokenSource.CancelAfter(timeout); + } + + public void Dispose() + { + sigInt?.Dispose(); + sigQuit?.Dispose(); + sigTerm?.Dispose(); + timeoutCancellationTokenSource.Dispose(); + } + } + + struct SyncAsyncDisposeWrapper(T value) : IDisposable + where T : IAsyncDisposable + { + public readonly T Value => value; + + public void Dispose() + { + value.DisposeAsync().AsTask().GetAwaiter().GetResult(); + } + } + + internal partial class ConsoleAppBuilder + { + public ConsoleAppBuilder() + { + } + + public void Add(string commandName, Delegate command) + { + AddCore(commandName, command); + } + + [System.Diagnostics.Conditional("DEBUG")] + public void Add() { } + + [System.Diagnostics.Conditional("DEBUG")] + public void Add(string commandPath) { } + + [System.Diagnostics.Conditional("DEBUG")] + public void UseFilter() where T : ConsoleAppFilter { } + + public void Run(string[] args) + { + BuildAndSetServiceProvider(); + RunCore(args); + } + + public Task RunAsync(string[] args) + { + BuildAndSetServiceProvider(); + Task? task = null; + RunAsyncCore(args, ref task!); + return task ?? Task.CompletedTask; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + partial void AddCore(string commandName, Delegate command); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + partial void RunCore(string[] args); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + partial void RunAsyncCore(string[] args, ref Task result); + + partial void BuildAndSetServiceProvider(); + + static partial void ShowHelp(int helpId); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static bool TryShowHelpOrVersion(ReadOnlySpan args, int requiredParameterCount, int helpId) + { + if (args.Length == 0) + { + if (requiredParameterCount == 0) return false; + + ShowHelp(helpId); + return true; + } + + if (args.Length == 1) + { + switch (args[0]) + { + case "--version": + ShowVersion(); + return true; + case "-h": + case "--help": + ShowHelp(helpId); + return true; + default: + break; + } + } + + return false; + } + } +} +"""; +} diff --git a/src/ConsoleAppFramework/ConsoleAppFramework.csproj b/src/ConsoleAppFramework/ConsoleAppFramework.csproj index 1be4fde..8f33a20 100644 --- a/src/ConsoleAppFramework/ConsoleAppFramework.csproj +++ b/src/ConsoleAppFramework/ConsoleAppFramework.csproj @@ -21,6 +21,12 @@ Zero Dependency, Zero Overhead, Zero Reflection, Zero Allocation, AOT Safe CLI Framework powered by C# Source Generator. Debug;Release + + + + + + diff --git a/src/ConsoleAppFramework/ConsoleAppFrameworkGeneratorOptions.cs b/src/ConsoleAppFramework/ConsoleAppFrameworkGeneratorOptions.cs deleted file mode 100644 index 2d0339b..0000000 --- a/src/ConsoleAppFramework/ConsoleAppFrameworkGeneratorOptions.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace ConsoleAppFramework; - -readonly record struct ConsoleAppFrameworkGeneratorOptions(bool DisableNamingConversion); \ No newline at end of file diff --git a/src/ConsoleAppFramework/ConsoleAppGenerator.cs b/src/ConsoleAppFramework/ConsoleAppGenerator.cs index e815d4e..7a275ea 100644 --- a/src/ConsoleAppFramework/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework/ConsoleAppGenerator.cs @@ -2,6 +2,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using System.Collections.Immutable; +using System.ComponentModel.Design; using System.Reflection; namespace ConsoleAppFramework; @@ -11,19 +12,51 @@ public partial class ConsoleAppGenerator : IIncrementalGenerator { public void Initialize(IncrementalGeneratorInitializationContext context) { + // Emit ConsoleApp.g.cs context.RegisterPostInitializationOutput(EmitConsoleAppTemplateSource); - // TODO: modify this. ConsoleApp.Create(Action configure) + // Emti ConfigureConfiguration/Logging/Services var hasDependencyInjection = context.MetadataReferencesProvider - .Where(x => - { - return x.Display?.EndsWith("Microsoft.Extensions.DependencyInjection.Abstractions.dll") ?? false; - }) .Collect() - .Select((x, ct) => x.Length != 0); + .Select((xs, _) => + { + var hasDependencyInjection = false; + var hasLogging = false; + var hasConfiguration = false; - context.RegisterSourceOutput(hasDependencyInjection, EmitConsoleAppCreateConfigure); + foreach (var x in xs) + { + if (hasDependencyInjection && hasLogging && hasConfiguration) break; + + var name = x.Display; + if (name == null) continue; + + if (!hasDependencyInjection && name.EndsWith("Microsoft.Extensions.DependencyInjection.Abstractions.dll")) + { + hasDependencyInjection = true; + continue; + } + + if (!hasLogging && name.EndsWith("Microsoft.Extensions.Logging.Abstractions.dll")) + { + hasLogging = true; + continue; + } + + if (!hasConfiguration && name.EndsWith("Microsoft.Extensions.Configuration.Abstractions.dll")) + { + hasConfiguration = true; + continue; + } + + } + return new DllReference(hasDependencyInjection, hasLogging, hasConfiguration); + }); + + context.RegisterSourceOutput(hasDependencyInjection, EmitConsoleAppConfigure); + + // get Options for Combine var generatorOptions = context.CompilationProvider.Select((compilation, token) => { foreach (var attr in compilation.Assembly.GetAttributes()) @@ -134,515 +167,11 @@ public void Initialize(IncrementalGeneratorInitializationContext context) context.RegisterSourceOutput(combined, EmitConsoleAppBuilder); } - public const string ConsoleAppBaseCode = """ -// -#nullable enable -namespace ConsoleAppFramework; - -using System; -using System.Text; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using System.Runtime.InteropServices; -using System.Runtime.CompilerServices; -using System.Diagnostics.CodeAnalysis; -using System.ComponentModel.DataAnnotations; - -#if !USE_EXTERNAL_CONSOLEAPP_ABSTRACTIONS - -internal interface IArgumentParser -{ - static abstract bool TryParse(ReadOnlySpan s, out T result); -} - -internal record class ConsoleAppContext(string CommandName, string[] Arguments, object? State); - -internal abstract class ConsoleAppFilter(ConsoleAppFilter next) -{ - protected readonly ConsoleAppFilter Next = next; - - public abstract Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken); -} - -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = false)] -internal sealed class ConsoleAppFilterAttribute : Attribute - where T : ConsoleAppFilter -{ -} - -internal sealed class ArgumentParseFailedException(string message) : Exception(message) -{ -} - -#endif - -[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] -internal sealed class FromServicesAttribute : Attribute -{ -} - -[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] -internal sealed class ArgumentAttribute : Attribute -{ -} - -[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -internal sealed class CommandAttribute : Attribute -{ - public string Command { get; } - - public CommandAttribute(string command) - { - this.Command = command; - } -} - -[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] -internal sealed class RegisterCommandsAttribute : Attribute -{ - public string CommandPath { get; } - - public RegisterCommandsAttribute() - { - this.CommandPath = ""; - } - - public RegisterCommandsAttribute(string commandPath) - { - this.CommandPath = commandPath; - } -} - -[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false, Inherited = false)] -public class ConsoleAppFrameworkGeneratorOptionsAttribute : Attribute -{ - public bool DisableNamingConversion { get; set; } -} - -internal static partial class ConsoleApp -{ - public static IServiceProvider? ServiceProvider { get; set; } - public static TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(5); - public static System.Text.Json.JsonSerializerOptions? JsonSerializerOptions { get; set; } - public static string? Version { get; set; } - - static Action? logAction; - public static Action Log - { - get => logAction ??= Console.WriteLine; - set => logAction = value; - } - - static Action? logErrorAction; - public static Action LogError - { - get => logErrorAction ??= (static msg => Log(msg)); - set => logErrorAction = value; - } - - /// - /// You can pass second argument that generates new Run overload. - /// ConsoleApp.Run(args, (int x, int y) => { });
- /// ConsoleApp.Run(args, Foo);
- /// ConsoleApp.Run(args, &Foo);
- ///
- public static void Run(string[] args) - { - } - - /// - /// You can pass second argument that generates new RunAsync overload. - /// ConsoleApp.RunAsync(args, (int x, int y) => { });
- /// ConsoleApp.RunAsync(args, Foo);
- /// ConsoleApp.RunAsync(args, &Foo);
- ///
- public static Task RunAsync(string[] args) - { - return Task.CompletedTask; - } - - public static ConsoleAppBuilder Create() => new ConsoleAppBuilder(); - - public static ConsoleAppBuilder Create(IServiceProvider serviceProvider) - { - ConsoleApp.ServiceProvider = serviceProvider; - return ConsoleApp.Create(); - } - - static void ThrowArgumentParseFailed(string argumentName, string value) - { - throw new ArgumentParseFailedException($"Argument '{argumentName}' failed to parse, provided value: {value}"); - } - - static void ThrowRequiredArgumentNotParsed(string name) - { - throw new ArgumentParseFailedException($"Required argument '{name}' was not specified."); - } - - static void ThrowArgumentNameNotFound(string argumentName) - { - throw new ArgumentParseFailedException($"Argument '{argumentName}' is not recognized."); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static bool TryIncrementIndex(ref int index, int length) - { - if ((index + 1) < length) - { - index += 1; - return true; - } - return false; - } - - static bool TryParseParamsArray(ReadOnlySpan args, ref T[] result, ref int i) - where T : IParsable - { - result = new T[args.Length - i]; - var resultIndex = 0; - for (; i < args.Length; i++) - { - if (!T.TryParse(args[i], null, out result[resultIndex++]!)) return false; - } - return true; - } - - static bool TrySplitParse(ReadOnlySpan s, out T[] result) - where T : ISpanParsable - { - if (s.StartsWith("[")) - { - try - { - result = System.Text.Json.JsonSerializer.Deserialize(s, JsonSerializerOptions)!; - return true; - } - catch - { - result = default!; - return false; - } - } - - var count = s.Count(',') + 1; - result = new T[count]; - - var source = s; - var destination = result.AsSpan(); - Span ranges = stackalloc Range[Math.Min(count, 128)]; - - while (true) - { - var splitCount = source.Split(ranges, ','); - var parseTo = splitCount; - if (splitCount == 128 && source[ranges[^1]].Contains(',')) - { - parseTo = splitCount - 1; - } - - for (int i = 0; i < parseTo; i++) - { - if (!T.TryParse(source[ranges[i]], null, out destination[i]!)) - { - return false; - } - } - destination = destination.Slice(parseTo); - - if (destination.Length != 0) - { - source = source[ranges[^1]]; - continue; - } - else - { - break; - } - } - - return true; - } - - static void ValidateParameter(object? value, ParameterInfo parameter, ValidationContext validationContext, ref StringBuilder? errorMessages) - { - validationContext.DisplayName = parameter.Name ?? ""; - validationContext.Items.Clear(); - - foreach (var validator in parameter.GetCustomAttributes(false)) - { - var result = validator.GetValidationResult(value, validationContext); - if (result != null) - { - if (errorMessages == null) - { - errorMessages = new StringBuilder(); - } - errorMessages.AppendLine(result.ErrorMessage); - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static bool TryShowHelpOrVersion(ReadOnlySpan args, int requiredParameterCount, int helpId) - { - if (args.Length == 0) - { - if (requiredParameterCount == 0) return false; - - ShowHelp(helpId); - return true; - } - - if (args.Length == 1) - { - switch (args[0]) - { - case "--version": - ShowVersion(); - return true; - case "-h": - case "--help": - ShowHelp(helpId); - return true; - default: - break; - } - } - - return false; - } - - static void ShowVersion() - { - if (Version != null) - { - Log(Version); - return; - } - - var asm = Assembly.GetEntryAssembly(); - var version = "1.0.0"; - var infoVersion = asm!.GetCustomAttribute(); - if (infoVersion != null) - { - version = infoVersion.InformationalVersion; - } - else - { - var asmVersion = asm!.GetCustomAttribute(); - if (asmVersion != null) - { - version = asmVersion.Version; - } - } - Log(version); - } - - static partial void ShowHelp(int helpId); - - static async Task RunWithFilterAsync(string commandName, string[] args, ConsoleAppFilter invoker) - { - using var posixSignalHandler = PosixSignalHandler.Register(Timeout); - try - { - await Task.Run(() => invoker.InvokeAsync(new ConsoleAppContext(commandName, args, null), posixSignalHandler.Token)).WaitAsync(posixSignalHandler.TimeoutToken); - } - catch (Exception ex) - { - if (ex is OperationCanceledException) - { - Environment.ExitCode = 130; - return; - } - - Environment.ExitCode = 1; - if (ex is ValidationException or ArgumentParseFailedException) - { - LogError(ex.Message); - } - else - { - LogError(ex.ToString()); - } - } - } - - sealed class PosixSignalHandler : IDisposable - { - public CancellationToken Token => cancellationTokenSource.Token; - public CancellationToken TimeoutToken => timeoutCancellationTokenSource.Token; - - CancellationTokenSource cancellationTokenSource; - CancellationTokenSource timeoutCancellationTokenSource; - TimeSpan timeout; - - PosixSignalRegistration? sigInt; - PosixSignalRegistration? sigQuit; - PosixSignalRegistration? sigTerm; - - PosixSignalHandler(TimeSpan timeout) - { - this.cancellationTokenSource = new CancellationTokenSource(); - this.timeoutCancellationTokenSource = new CancellationTokenSource(); - this.timeout = timeout; - } - - public static PosixSignalHandler Register(TimeSpan timeout) - { - var handler = new PosixSignalHandler(timeout); - - Action handleSignal = handler.HandlePosixSignal; - - handler.sigInt = PosixSignalRegistration.Create(PosixSignal.SIGINT, handleSignal); - handler.sigQuit = PosixSignalRegistration.Create(PosixSignal.SIGQUIT, handleSignal); - handler.sigTerm = PosixSignalRegistration.Create(PosixSignal.SIGTERM, handleSignal); - - return handler; - } - - void HandlePosixSignal(PosixSignalContext context) - { - context.Cancel = true; - cancellationTokenSource.Cancel(); - timeoutCancellationTokenSource.CancelAfter(timeout); - } - - public void Dispose() - { - sigInt?.Dispose(); - sigQuit?.Dispose(); - sigTerm?.Dispose(); - timeoutCancellationTokenSource.Dispose(); - } - } - - struct SyncAsyncDisposeWrapper(T value) : IDisposable - where T : IAsyncDisposable - { - public readonly T Value => value; - - public void Dispose() - { - value.DisposeAsync().AsTask().GetAwaiter().GetResult(); - } - } - - internal partial struct ConsoleAppBuilder - { - public ConsoleAppBuilder() - { - } - - public void Add(string commandName, Delegate command) - { - AddCore(commandName, command); - } - - [System.Diagnostics.Conditional("DEBUG")] - public void Add() { } - - [System.Diagnostics.Conditional("DEBUG")] - public void Add(string commandPath) { } - - [System.Diagnostics.Conditional("DEBUG")] - public void UseFilter() where T : ConsoleAppFilter { } - - public void Run(string[] args) - { - RunCore(args); - } - - public Task RunAsync(string[] args) - { - Task? task = null; - RunAsyncCore(args, ref task!); - return task ?? Task.CompletedTask; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - partial void AddCore(string commandName, Delegate command); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - partial void RunCore(string[] args); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - partial void RunAsyncCore(string[] args, ref Task result); - - static partial void ShowHelp(int helpId); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static bool TryShowHelpOrVersion(ReadOnlySpan args, int requiredParameterCount, int helpId) - { - if (args.Length == 0) - { - if (requiredParameterCount == 0) return false; - - ShowHelp(helpId); - return true; - } - - if (args.Length == 1) - { - switch (args[0]) - { - case "--version": - ShowVersion(); - return true; - case "-h": - case "--help": - ShowHelp(helpId); - return true; - default: - break; - } - } - - return false; - } - } -} -"""; - static void EmitConsoleAppTemplateSource(IncrementalGeneratorPostInitializationContext context) { - context.AddSource("ConsoleApp.cs", ConsoleAppBaseCode); + context.AddSource("ConsoleApp.cs", ConsoleAppBaseCode.InitializationCode); } - const string GeneratedCodeHeader = """ -// -#nullable enable -#pragma warning disable CS0108 // hides inherited member -#pragma warning disable CS0162 // Unreachable code -#pragma warning disable CS0164 // This label has not been referenced -#pragma warning disable CS0219 // Variable assigned but never used -#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. -#pragma warning disable CS8601 // Possible null reference assignment -#pragma warning disable CS8602 -#pragma warning disable CS8604 // Possible null reference argument for parameter -#pragma warning disable CS8619 -#pragma warning disable CS8620 -#pragma warning disable CS8631 // The type cannot be used as type parameter in the generic type or method -#pragma warning disable CS8765 // Nullability of type of parameter -#pragma warning disable CS9074 // The 'scoped' modifier of parameter doesn't match overridden or implemented member -#pragma warning disable CA1050 // Declare types in namespaces. -#pragma warning disable CS1998 -#pragma warning disable CS8625 - -namespace ConsoleAppFramework; - -using System; -using System.Text; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using System.Runtime.InteropServices; -using System.Runtime.CompilerServices; -using System.Diagnostics.CodeAnalysis; -using System.ComponentModel.DataAnnotations; - -"""; - static void EmitConsoleAppRun(SourceProductionContext sourceProductionContext, CommanContext commandContext) { if (commandContext.DiagnosticReporter.HasDiagnostics) @@ -660,7 +189,7 @@ static void EmitConsoleAppRun(SourceProductionContext sourceProductionContext, C } var sb = new SourceBuilder(0); - sb.AppendLine(GeneratedCodeHeader); + sb.AppendLine(ConsoleAppBaseCode.GeneratedCodeHeader); using (sb.BeginBlock("internal static partial class ConsoleApp")) { var emitter = new Emitter(); @@ -670,7 +199,7 @@ static void EmitConsoleAppRun(SourceProductionContext sourceProductionContext, C sourceProductionContext.AddSource("ConsoleApp.Run.g.cs", sb.ToString()); var help = new SourceBuilder(0); - help.AppendLine(GeneratedCodeHeader); + help.AppendLine(ConsoleAppBaseCode.GeneratedCodeHeader); using (help.BeginBlock("internal static partial class ConsoleApp")) { var emitter = new Emitter(); @@ -694,7 +223,7 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex if (!hasRun && !hasRunAsync) return; var sb = new SourceBuilder(0); - sb.AppendLine(GeneratedCodeHeader); + sb.AppendLine(ConsoleAppBaseCode.GeneratedCodeHeader); var delegateSignatures = new List(); @@ -730,9 +259,9 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex // Build Help var help = new SourceBuilder(0); - help.AppendLine(GeneratedCodeHeader); + help.AppendLine(ConsoleAppBaseCode.GeneratedCodeHeader); using (help.BeginBlock("internal static partial class ConsoleApp")) - using (help.BeginBlock("internal partial struct ConsoleAppBuilder")) + using (help.BeginBlock("internal partial class ConsoleAppBuilder")) { var emitter = new Emitter(); emitter.EmitHelp(help, commandIds!); @@ -740,35 +269,38 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex sourceProductionContext.AddSource("ConsoleApp.Builder.Help.g.cs", help.ToString()); } - static void EmitConsoleAppCreateConfigure(SourceProductionContext sourceProductionContext, bool hasDependencyInjection) + static void EmitConsoleAppConfigure(SourceProductionContext sourceProductionContext, DllReference dllReference) { - var code = """ -// -#nullable enable -namespace ConsoleAppFramework; - -using System; -using Microsoft.Extensions.DependencyInjection; + if (!dllReference.HasDependencyInjection && !dllReference.HasLogging && !dllReference.HasConfiguration) + { + sourceProductionContext.AddSource("ConsoleApp.Builder.Configure.g.cs", ""); + return; + } -internal static partial class ConsoleApp -{ - public static ConsoleAppBuilder Create(Action configure) - { - var services = new ServiceCollection(); - configure(services); - ConsoleApp.ServiceProvider = services.BuildServiceProvider(); - return ConsoleApp.Create(); - } -} -"""; + var sb = new SourceBuilder(0); + sb.AppendLine(ConsoleAppBaseCode.GeneratedCodeHeader); + + if (dllReference.HasDependencyInjection) + { + sb.AppendLine("using Microsoft.Extensions.DependencyInjection;"); + } + if (dllReference.HasLogging) + { + sb.AppendLine("using Microsoft.Extensions.Logging;"); + } + if (dllReference.HasConfiguration) + { + sb.AppendLine("using Microsoft.Extensions.Configuration;"); + } - // emit empty if not exists - if (!hasDependencyInjection) + using (sb.BeginBlock("internal static partial class ConsoleApp")) + using (sb.BeginBlock("internal partial class ConsoleAppBuilder")) { - code = ""; + var emitter = new Emitter(); + emitter.EmitConfigure(sb, dllReference); } - sourceProductionContext.AddSource("ConsoleApp.Create.g.cs", code); + sourceProductionContext.AddSource("ConsoleApp.Builder.Configure.g.cs", sb.ToString()); } class CommanContext(Command? command, bool isAsync, DiagnosticReporter diagnosticReporter, InvocationExpressionSyntax node) : IEquatable diff --git a/src/ConsoleAppFramework/Emitter.cs b/src/ConsoleAppFramework/Emitter.cs index 57220d7..a1c8cd7 100644 --- a/src/ConsoleAppFramework/Emitter.cs +++ b/src/ConsoleAppFramework/Emitter.cs @@ -334,7 +334,7 @@ public void EmitBuilder(SourceBuilder sb, CommandWithId[] commandIds, bool emitS var commandGroup = commandIds.ToLookup(x => x.Command.Name.Split(' ')[0]); var hasRootCommand = commandIds.Any(x => x.Command.IsRootCommand); - using (sb.BeginBlock("partial struct ConsoleAppBuilder")) + using (sb.BeginBlock("partial class ConsoleAppBuilder")) { // fields: 'Action command0 = default!;' foreach (var item in commandIds.Where(x => x.FieldType != null)) @@ -605,6 +605,184 @@ public void EmitHelp(SourceBuilder sb, CommandWithId[] commands) } } + public void EmitConfigure(SourceBuilder sb, DllReference dllReference) + { + // configuration + if (dllReference.HasConfiguration) + { + sb.AppendLine("bool requireConfiguration;"); + sb.AppendLine("IConfiguration? configuration;"); + + sb.AppendLine(); + sb.AppendLine("/// Create configuration with SetBasePath(Directory.GetCurrentDirectory()) and AddJsonFile(appsettings.json)."); + using (sb.BeginBlock("public ConsoleApp.ConsoleAppBuilder ConfigureDefaultConfiguration()")) + { + sb.AppendLine("var config = new ConfigurationBuilder();"); + sb.AppendLine("config.SetBasePath(System.IO.Directory.GetCurrentDirectory());"); + sb.AppendLine("config.AddJsonFile(\"appsettings.json\", optional: true);"); + sb.AppendLine("configuration = config.Build();"); + sb.AppendLine("return this;"); + } + + sb.AppendLine(); + sb.AppendLine("/// Create configuration with SetBasePath(Directory.GetCurrentDirectory()) and AddJsonFile(appsettings.json)."); + using (sb.BeginBlock("public ConsoleApp.ConsoleAppBuilder ConfigureDefaultConfiguration(Action configure)")) + { + sb.AppendLine("var config = new ConfigurationBuilder();"); + sb.AppendLine("config.SetBasePath(System.IO.Directory.GetCurrentDirectory());"); + sb.AppendLine("config.AddJsonFile(\"appsettings.json\", optional: true);"); + sb.AppendLine("configure(config);"); + sb.AppendLine("configuration = config.Build();"); + sb.AppendLine("return this;"); + } + + sb.AppendLine(); + using (sb.BeginBlock("public ConsoleApp.ConsoleAppBuilder ConfigureEmptyConfiguration(Action configure)")) + { + sb.AppendLine("var config = new ConfigurationBuilder();"); + sb.AppendLine("configure(config);"); + sb.AppendLine("configuration = config.Build();"); + sb.AppendLine("return this;"); + } + sb.AppendLine(); + } + + // DependencyInjection + if (dllReference.HasDependencyInjection) + { + if (dllReference.HasConfiguration) + { + sb.AppendLine("Action? configureServices;"); + } + else + { + sb.AppendLine("Action? configureServices;"); + } + + sb.AppendLine(); + using (sb.BeginBlock("public ConsoleApp.ConsoleAppBuilder ConfigureServices(Action configure)")) + { + if (dllReference.HasConfiguration) + { + sb.AppendLine("this.configureServices = (_, services) => configure(services);"); + } + else + { + sb.AppendLine("this.configureServices = configure;"); + } + sb.AppendLine("return this;"); + } + + if (dllReference.HasConfiguration) + { + sb.AppendLine(); + using (sb.BeginBlock("public ConsoleApp.ConsoleAppBuilder ConfigureServices(Action configure)")) + { + sb.AppendLine("this.requireConfiguration = true;"); + sb.AppendLine("this.configureServices = configure;"); + sb.AppendLine("return this;"); + } + } + sb.AppendLine(); + } + + // Logging + if (dllReference.HasLogging) + { + if (dllReference.HasConfiguration) + { + sb.AppendLine("Action? configureLogging;"); + } + else + { + sb.AppendLine("Action? configureLogging;"); + } + + sb.AppendLine(); + using (sb.BeginBlock("public ConsoleApp.ConsoleAppBuilder ConfigureLogging(Action configure)")) + { + if (dllReference.HasConfiguration) + { + sb.AppendLine("this.configureLogging = (_, logging) => configure(logging);"); + } + else + { + sb.AppendLine("this.configureLogging = configure;"); + } + sb.AppendLine("return this;"); + } + + if (dllReference.HasConfiguration) + { + sb.AppendLine(); + using (sb.BeginBlock("public ConsoleApp.ConsoleAppBuilder ConfigureLogging(Action configure)")) + { + sb.AppendLine("this.requireConfiguration = true;"); + sb.AppendLine("this.configureLogging = configure;"); + sb.AppendLine("return this;"); + } + } + sb.AppendLine(); + } + + // Build + using (sb.BeginBlock("partial void BuildAndSetServiceProvider()")) + { + if (dllReference.HasDependencyInjection && dllReference.HasLogging) + { + sb.AppendLine("if (configureServices == null && configureLogging == null) return;"); + } + else if (dllReference.HasDependencyInjection) + { + sb.AppendLine("if (configureServices == null) return;"); + } + + if (dllReference.HasDependencyInjection) + { + if (dllReference.HasConfiguration) + { + sb.AppendLine("var config = configuration;"); + using (sb.BeginBlock("if (requireConfiguration && config == null)")) + { + sb.AppendLine("config = new ConfigurationRoot(Array.Empty());"); + } + } + + sb.AppendLine("var services = new ServiceCollection();"); + if (dllReference.HasConfiguration) + { + sb.AppendLine("configureServices?.Invoke(configuration!, services);"); + } + else + { + sb.AppendLine("configureServices?.Invoke(services);"); + } + + if (dllReference.HasLogging) + { + using (sb.BeginBlock("if (configureLogging != null)")) + { + sb.AppendLine("var configure = configureLogging;"); + using (sb.BeginIndent("services.AddLogging(logging => {")) + { + if (dllReference.HasConfiguration) + { + sb.AppendLine("configure!(config!, logging);"); + } + else + { + sb.AppendLine("configure!(logging);"); + } + } + sb.AppendLine("});"); + } + } + + sb.AppendLine("ConsoleApp.ServiceProvider = services.BuildServiceProvider();"); + } + } + } + internal record CommandWithId(string? FieldType, Command Command, int Id) { public static string BuildCustomDelegateTypeName(int id) diff --git a/src/ConsoleAppFramework/SourceGeneratorContexts.cs b/src/ConsoleAppFramework/SourceGeneratorContexts.cs new file mode 100644 index 0000000..2dec396 --- /dev/null +++ b/src/ConsoleAppFramework/SourceGeneratorContexts.cs @@ -0,0 +1,5 @@ +namespace ConsoleAppFramework; + +readonly record struct ConsoleAppFrameworkGeneratorOptions(bool DisableNamingConversion); + +readonly record struct DllReference(bool HasDependencyInjection, bool HasLogging, bool HasConfiguration); \ No newline at end of file diff --git a/tests/ConsoleAppFramework.GeneratorTests/GeneratorOptionsTest.cs b/tests/ConsoleAppFramework.GeneratorTests/GeneratorOptionsTest.cs new file mode 100644 index 0000000..646f4dd --- /dev/null +++ b/tests/ConsoleAppFramework.GeneratorTests/GeneratorOptionsTest.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ConsoleAppFramework.GeneratorTests; + +public class GeneratorOptionsTest(ITestOutputHelper output) +{ + VerifyHelper verifier = new VerifyHelper(output, "CAF"); + + [Fact] + public void DisableNamingConversionRun() + { + verifier.Execute(""" +[assembly: ConsoleAppFrameworkGeneratorOptions(DisableNamingConversion = true)] + +ConsoleApp.Run(args, (int fooBarBaz) => { Console.Write(fooBarBaz); }); +""", args: "--fooBarBaz 100", expected: "100"); + + verifier.Execute(""" +[assembly: ConsoleAppFrameworkGeneratorOptions(DisableNamingConversion = false)] + +ConsoleApp.Run(args, (int fooBarBaz) => { Console.Write(fooBarBaz); }); +""", args: "--foo-bar-baz 100", expected: "100"); + } + + [Fact] + public void DisableNamingConversionBuilder() + { + verifier.Execute(""" +[assembly: ConsoleAppFrameworkGeneratorOptions(DisableNamingConversion = true)] + +var app = ConsoleApp.Create(); +app.Add(); +app.Run(args); + +class Commands +{ + public void FooBarBaz(int hogeMoge, int takoYaki) + { + Console.Write(hogeMoge + takoYaki); + } +} +""", args: "FooBarBaz --hogeMoge 100 --takoYaki 200", expected: "300"); + + verifier.Execute(""" +[assembly: ConsoleAppFrameworkGeneratorOptions(DisableNamingConversion = false)] + +var app = ConsoleApp.Create(); +app.Add(); +app.Run(args); + +class Commands +{ + public void FooBarBaz(int hogeMoge, int takoYaki) + { + Console.Write(hogeMoge + takoYaki); + } +} +""", args: "foo-bar-baz --hoge-moge 100 --tako-yaki 200", expected: "300"); + } +}