From fb7259a8580753e4a767e042488e9a339118f86b Mon Sep 17 00:00:00 2001 From: neuecc Date: Mon, 13 May 2024 09:58:07 +0900 Subject: [PATCH 01/54] v5 generator --- ConsoleAppFramework.sln | 21 ++- sandbox/AspNetApp/AspNetApp.csproj | 8 - sandbox/AspNetApp/Program.cs | 26 --- sandbox/AspNetApp/Startup.cs | 40 ----- .../AspNetApp/appsettings.Development.json | 9 - sandbox/AspNetApp/appsettings.json | 10 -- sandbox/CliFrameworkBenchmark/Benchmark.cs | 58 +++++++ .../CliFrameworkBenchmark.csproj | 23 +++ .../Commands/CliFxCommand.cs | 19 ++ .../Commands/CliprCommand.cs | 19 ++ .../Commands/CoconaCommand.cs | 14 ++ .../Commands/CommandLineParserCommand.cs | 17 ++ .../Commands/ConsoleAppFrameworkCommand.cs | 16 ++ .../Commands/McMasterCommand.cs | 15 ++ .../Commands/PowerArgsCommand.cs | 19 ++ .../Commands/SystemCommandLineCommand.cs | 32 ++++ sandbox/CliFrameworkBenchmark/Program.cs | 18 ++ sandbox/CliFrameworkBenchmark/README.md | 5 + .../GeneratorSandbox/GeneratorSandbox.csproj | 17 ++ sandbox/GeneratorSandbox/Program.cs | 162 ++++++++++++++++++ .../MultiContainedApp.csproj | 2 +- .../SingleContainedApp.csproj | 2 +- .../SingleContainedAppWithConfig.csproj | 58 +++---- src/ConsoleAppFramework5/Command.cs | 153 +++++++++++++++++ .../ConsoleAppFramework5.csproj | 22 +++ .../ConsoleAppGenerator.cs | 143 ++++++++++++++++ src/ConsoleAppFramework5/Emitter.cs | 95 ++++++++++ src/ConsoleAppFramework5/Parser.cs | 145 ++++++++++++++++ .../Properties/launchSettings.json | 8 + src/ConsoleAppFramework5/RoslynExtensions.cs | 18 ++ src/ConsoleAppFramework5/WellKnownTypes.cs | 40 +++++ 31 files changed, 1103 insertions(+), 131 deletions(-) delete mode 100644 sandbox/AspNetApp/AspNetApp.csproj delete mode 100644 sandbox/AspNetApp/Program.cs delete mode 100644 sandbox/AspNetApp/Startup.cs delete mode 100644 sandbox/AspNetApp/appsettings.Development.json delete mode 100644 sandbox/AspNetApp/appsettings.json create mode 100644 sandbox/CliFrameworkBenchmark/Benchmark.cs create mode 100644 sandbox/CliFrameworkBenchmark/CliFrameworkBenchmark.csproj create mode 100644 sandbox/CliFrameworkBenchmark/Commands/CliFxCommand.cs create mode 100644 sandbox/CliFrameworkBenchmark/Commands/CliprCommand.cs create mode 100644 sandbox/CliFrameworkBenchmark/Commands/CoconaCommand.cs create mode 100644 sandbox/CliFrameworkBenchmark/Commands/CommandLineParserCommand.cs create mode 100644 sandbox/CliFrameworkBenchmark/Commands/ConsoleAppFrameworkCommand.cs create mode 100644 sandbox/CliFrameworkBenchmark/Commands/McMasterCommand.cs create mode 100644 sandbox/CliFrameworkBenchmark/Commands/PowerArgsCommand.cs create mode 100644 sandbox/CliFrameworkBenchmark/Commands/SystemCommandLineCommand.cs create mode 100644 sandbox/CliFrameworkBenchmark/Program.cs create mode 100644 sandbox/CliFrameworkBenchmark/README.md create mode 100644 sandbox/GeneratorSandbox/GeneratorSandbox.csproj create mode 100644 sandbox/GeneratorSandbox/Program.cs create mode 100644 src/ConsoleAppFramework5/Command.cs create mode 100644 src/ConsoleAppFramework5/ConsoleAppFramework5.csproj create mode 100644 src/ConsoleAppFramework5/ConsoleAppGenerator.cs create mode 100644 src/ConsoleAppFramework5/Emitter.cs create mode 100644 src/ConsoleAppFramework5/Parser.cs create mode 100644 src/ConsoleAppFramework5/Properties/launchSettings.json create mode 100644 src/ConsoleAppFramework5/RoslynExtensions.cs create mode 100644 src/ConsoleAppFramework5/WellKnownTypes.cs diff --git a/ConsoleAppFramework.sln b/ConsoleAppFramework.sln index b162dfc..5bbdbb3 100644 --- a/ConsoleAppFramework.sln +++ b/ConsoleAppFramework.sln @@ -28,12 +28,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ReadMe.md = ReadMe.md EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetApp", "sandbox\AspNetApp\AspNetApp.csproj", "{E065696C-9DF6-4E68-933B-BFA0165781DD}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Net6WebApp", "sandbox\Net6WebApp\Net6WebApp.csproj", "{48781D9F-D3E0-4A72-ADD1-A47ECDC23F0A}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Net6Console", "sandbox\Net6Console\Net6Console.csproj", "{19E33348-979A-4283-A74D-0844CC384A88}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleAppFramework5", "src\ConsoleAppFramework5\ConsoleAppFramework5.csproj", "{09BEEA7B-B6D3-4011-BCAB-6DF976713695}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GeneratorSandbox", "sandbox\GeneratorSandbox\GeneratorSandbox.csproj", "{ACDA48BA-0BFE-4917-B335-7836DAA5929A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -60,10 +62,6 @@ Global {AF15C841-5D45-4E61-BFCE-A6E6B7BA7629}.Debug|Any CPU.Build.0 = Debug|Any CPU {AF15C841-5D45-4E61-BFCE-A6E6B7BA7629}.Release|Any CPU.ActiveCfg = Release|Any CPU {AF15C841-5D45-4E61-BFCE-A6E6B7BA7629}.Release|Any CPU.Build.0 = Release|Any CPU - {E065696C-9DF6-4E68-933B-BFA0165781DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E065696C-9DF6-4E68-933B-BFA0165781DD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E065696C-9DF6-4E68-933B-BFA0165781DD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E065696C-9DF6-4E68-933B-BFA0165781DD}.Release|Any CPU.Build.0 = Release|Any CPU {48781D9F-D3E0-4A72-ADD1-A47ECDC23F0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {48781D9F-D3E0-4A72-ADD1-A47ECDC23F0A}.Debug|Any CPU.Build.0 = Debug|Any CPU {48781D9F-D3E0-4A72-ADD1-A47ECDC23F0A}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -72,6 +70,14 @@ Global {19E33348-979A-4283-A74D-0844CC384A88}.Debug|Any CPU.Build.0 = Debug|Any CPU {19E33348-979A-4283-A74D-0844CC384A88}.Release|Any CPU.ActiveCfg = Release|Any CPU {19E33348-979A-4283-A74D-0844CC384A88}.Release|Any CPU.Build.0 = Release|Any CPU + {09BEEA7B-B6D3-4011-BCAB-6DF976713695}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {09BEEA7B-B6D3-4011-BCAB-6DF976713695}.Debug|Any CPU.Build.0 = Debug|Any CPU + {09BEEA7B-B6D3-4011-BCAB-6DF976713695}.Release|Any CPU.ActiveCfg = Release|Any CPU + {09BEEA7B-B6D3-4011-BCAB-6DF976713695}.Release|Any CPU.Build.0 = Release|Any CPU + {ACDA48BA-0BFE-4917-B335-7836DAA5929A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ACDA48BA-0BFE-4917-B335-7836DAA5929A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ACDA48BA-0BFE-4917-B335-7836DAA5929A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ACDA48BA-0BFE-4917-B335-7836DAA5929A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -82,9 +88,10 @@ Global {5E16EBD8-3396-4952-9B2D-DD2E2E3C883B} = {A2CF2984-E8E2-48FC-B5A1-58D74A2467E6} {0B3BE82E-753A-415D-AD4E-90350C6E5C3D} = {A2CF2984-E8E2-48FC-B5A1-58D74A2467E6} {AF15C841-5D45-4E61-BFCE-A6E6B7BA7629} = {AAD2D900-C305-4449-A9FC-6C7696FFEDFA} - {E065696C-9DF6-4E68-933B-BFA0165781DD} = {A2CF2984-E8E2-48FC-B5A1-58D74A2467E6} {48781D9F-D3E0-4A72-ADD1-A47ECDC23F0A} = {A2CF2984-E8E2-48FC-B5A1-58D74A2467E6} {19E33348-979A-4283-A74D-0844CC384A88} = {A2CF2984-E8E2-48FC-B5A1-58D74A2467E6} + {09BEEA7B-B6D3-4011-BCAB-6DF976713695} = {1F399F98-7439-4F05-847B-CC1267B4B7F2} + {ACDA48BA-0BFE-4917-B335-7836DAA5929A} = {A2CF2984-E8E2-48FC-B5A1-58D74A2467E6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7F3E353A-C125-4020-8481-11DC6496358C} diff --git a/sandbox/AspNetApp/AspNetApp.csproj b/sandbox/AspNetApp/AspNetApp.csproj deleted file mode 100644 index d61c2e1..0000000 --- a/sandbox/AspNetApp/AspNetApp.csproj +++ /dev/null @@ -1,8 +0,0 @@ - - - - net5.0 - false - - - diff --git a/sandbox/AspNetApp/Program.cs b/sandbox/AspNetApp/Program.cs deleted file mode 100644 index 265f310..0000000 --- a/sandbox/AspNetApp/Program.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace AspNetApp -{ - public class Program - { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); - } -} diff --git a/sandbox/AspNetApp/Startup.cs b/sandbox/AspNetApp/Startup.cs deleted file mode 100644 index 969f3a5..0000000 --- a/sandbox/AspNetApp/Startup.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace AspNetApp -{ - public class Startup - { - // This method gets called by the runtime. Use this method to add services to the container. - // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 - public void ConfigureServices(IServiceCollection services) - { - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseRouting(); - - app.UseEndpoints(endpoints => - { - endpoints.MapGet("/", async context => - { - await context.Response.WriteAsync("Hello World!"); - }); - }); - } - } -} diff --git a/sandbox/AspNetApp/appsettings.Development.json b/sandbox/AspNetApp/appsettings.Development.json deleted file mode 100644 index 8983e0f..0000000 --- a/sandbox/AspNetApp/appsettings.Development.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - } -} diff --git a/sandbox/AspNetApp/appsettings.json b/sandbox/AspNetApp/appsettings.json deleted file mode 100644 index d9d9a9b..0000000 --- a/sandbox/AspNetApp/appsettings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - }, - "AllowedHosts": "*" -} diff --git a/sandbox/CliFrameworkBenchmark/Benchmark.cs b/sandbox/CliFrameworkBenchmark/Benchmark.cs new file mode 100644 index 0000000..379be2a --- /dev/null +++ b/sandbox/CliFrameworkBenchmark/Benchmark.cs @@ -0,0 +1,58 @@ +// This benchmark project is based on CliFx.Benchmarks. +// https://github.com/Tyrrrz/CliFx/tree/master/CliFx.Benchmarks/ + +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Order; +using CliFx; +using Cocona.Benchmark.External.Commands; +using CommandLine; +using ConsoleAppFramework; +using Microsoft.Extensions.Hosting; + +namespace Cocona.Benchmark.External; + +[SimpleJob] +[RankColumn] +[Orderer(SummaryOrderPolicy.FastestToSlowest)] +public class Benchmark +{ + private static readonly string[] Arguments = { "--str", "hello world", "-i", "13", "-b" }; + + [Benchmark(Description = "Cocona.Lite", Baseline = true)] + public async Task ExecuteWithCoconaLite() => + await Cocona.CoconaLiteApp.RunAsync(Arguments); + + [Benchmark(Description = "Cocona")] + public async ValueTask ExecuteWithCocona() => + await Cocona.CoconaApp.RunAsync(Arguments); + + [Benchmark(Description = "ConsoleAppFramework")] + public async ValueTask ExecuteWithConsoleAppFramework() => + await Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(Arguments); + + [Benchmark(Description = "CliFx")] + public async ValueTask ExecuteWithCliFx() => + await new CliApplicationBuilder().AddCommand(typeof(CliFxCommand)).Build().RunAsync(Arguments); + + [Benchmark(Description = "System.CommandLine")] + public async Task ExecuteWithSystemCommandLine() => + await new SystemCommandLineCommand().ExecuteAsync(Arguments); + + [Benchmark(Description = "McMaster.Extensions.CommandLineUtils")] + public int ExecuteWithMcMaster() => + McMaster.Extensions.CommandLineUtils.CommandLineApplication.Execute(Arguments); + + [Benchmark(Description = "CommandLineParser")] + public void ExecuteWithCommandLineParser() => + new Parser() + .ParseArguments(Arguments, typeof(CommandLineParserCommand)) + .WithParsed(c => c.Execute()); + + [Benchmark(Description = "PowerArgs")] + public void ExecuteWithPowerArgs() => + PowerArgs.Args.InvokeMain(Arguments); + + [Benchmark(Description = "Clipr")] + public void ExecuteWithClipr() => + clipr.CliParser.Parse(Arguments).Execute(); +} \ No newline at end of file diff --git a/sandbox/CliFrameworkBenchmark/CliFrameworkBenchmark.csproj b/sandbox/CliFrameworkBenchmark/CliFrameworkBenchmark.csproj new file mode 100644 index 0000000..59d9ef9 --- /dev/null +++ b/sandbox/CliFrameworkBenchmark/CliFrameworkBenchmark.csproj @@ -0,0 +1,23 @@ + + + + Exe + net8.0 + enable + annotations + + + + + + + + + + + + + + + + diff --git a/sandbox/CliFrameworkBenchmark/Commands/CliFxCommand.cs b/sandbox/CliFrameworkBenchmark/Commands/CliFxCommand.cs new file mode 100644 index 0000000..6d73e06 --- /dev/null +++ b/sandbox/CliFrameworkBenchmark/Commands/CliFxCommand.cs @@ -0,0 +1,19 @@ +using CliFx.Attributes; +using CliFx.Infrastructure; + +namespace Cocona.Benchmark.External.Commands; + +[CliFx.Attributes.Command] +public class CliFxCommand : CliFx.ICommand +{ + [CommandOption("str", 's')] + public string? StrOption { get; set; } + + [CommandOption("int", 'i')] + public int IntOption { get; set; } + + [CommandOption("bool", 'b')] + public bool BoolOption { get; set; } + + public ValueTask ExecuteAsync(IConsole console) => ValueTask.CompletedTask; +} diff --git a/sandbox/CliFrameworkBenchmark/Commands/CliprCommand.cs b/sandbox/CliFrameworkBenchmark/Commands/CliprCommand.cs new file mode 100644 index 0000000..3a5b9ce --- /dev/null +++ b/sandbox/CliFrameworkBenchmark/Commands/CliprCommand.cs @@ -0,0 +1,19 @@ +using clipr; + +namespace Cocona.Benchmark.External.Commands; + +public class CliprCommand +{ + [NamedArgument('s', "str")] + public string? StrOption { get; set; } + + [NamedArgument('i', "int")] + public int IntOption { get; set; } + + [NamedArgument('b', "bool", Constraint = NumArgsConstraint.Optional, Const = true)] + public bool BoolOption { get; set; } + + public void Execute() + { + } +} \ No newline at end of file diff --git a/sandbox/CliFrameworkBenchmark/Commands/CoconaCommand.cs b/sandbox/CliFrameworkBenchmark/Commands/CoconaCommand.cs new file mode 100644 index 0000000..4e299f0 --- /dev/null +++ b/sandbox/CliFrameworkBenchmark/Commands/CoconaCommand.cs @@ -0,0 +1,14 @@ +namespace Cocona.Benchmark.External.Commands; + +public class CoconaCommand +{ + public void Execute( + [global::Cocona.Option("str", new []{'s'})] + string? strOption, + [global::Cocona.Option("int", new []{'i'})] + int intOption, + [global::Cocona.Option("bool", new []{'b'})] + bool boolOption) + { + } +} \ No newline at end of file diff --git a/sandbox/CliFrameworkBenchmark/Commands/CommandLineParserCommand.cs b/sandbox/CliFrameworkBenchmark/Commands/CommandLineParserCommand.cs new file mode 100644 index 0000000..b15dc2f --- /dev/null +++ b/sandbox/CliFrameworkBenchmark/Commands/CommandLineParserCommand.cs @@ -0,0 +1,17 @@ +namespace Cocona.Benchmark.External.Commands; + +public class CommandLineParserCommand +{ + [global::CommandLine.Option('s', "str")] + public string? StrOption { get; set; } + + [global::CommandLine.Option('i', "int")] + public int IntOption { get; set; } + + [global::CommandLine.Option('b', "bool")] + public bool BoolOption { get; set; } + + public void Execute() + { + } +} \ No newline at end of file diff --git a/sandbox/CliFrameworkBenchmark/Commands/ConsoleAppFrameworkCommand.cs b/sandbox/CliFrameworkBenchmark/Commands/ConsoleAppFrameworkCommand.cs new file mode 100644 index 0000000..b85275a --- /dev/null +++ b/sandbox/CliFrameworkBenchmark/Commands/ConsoleAppFrameworkCommand.cs @@ -0,0 +1,16 @@ +using ConsoleAppFramework; + +namespace Cocona.Benchmark.External.Commands; + +public class ConsoleAppFrameworkCommand : ConsoleAppBase +{ + public void Execute( + [global::ConsoleAppFramework.Option("s")] + string? str, + [global::ConsoleAppFramework.Option("i")] + int intOption, + [global::ConsoleAppFramework.Option("b")] + bool boolOption) + { + } +} \ No newline at end of file diff --git a/sandbox/CliFrameworkBenchmark/Commands/McMasterCommand.cs b/sandbox/CliFrameworkBenchmark/Commands/McMasterCommand.cs new file mode 100644 index 0000000..d9ff0d7 --- /dev/null +++ b/sandbox/CliFrameworkBenchmark/Commands/McMasterCommand.cs @@ -0,0 +1,15 @@ +namespace Cocona.Benchmark.External.Commands; + +public class McMasterCommand +{ + [McMaster.Extensions.CommandLineUtils.Option("--str|-s")] + public string? StrOption { get; set; } + + [McMaster.Extensions.CommandLineUtils.Option("--int|-i")] + public int IntOption { get; set; } + + [McMaster.Extensions.CommandLineUtils.Option("--bool|-b")] + public bool BoolOption { get; set; } + + public int OnExecute() => 0; +} \ No newline at end of file diff --git a/sandbox/CliFrameworkBenchmark/Commands/PowerArgsCommand.cs b/sandbox/CliFrameworkBenchmark/Commands/PowerArgsCommand.cs new file mode 100644 index 0000000..ce18da5 --- /dev/null +++ b/sandbox/CliFrameworkBenchmark/Commands/PowerArgsCommand.cs @@ -0,0 +1,19 @@ +using PowerArgs; + +namespace Cocona.Benchmark.External.Commands; + +public class PowerArgsCommand +{ + [ArgShortcut("--str"), ArgShortcut("-s")] + public string? StrOption { get; set; } + + [ArgShortcut("--int"), ArgShortcut("-i")] + public int IntOption { get; set; } + + [ArgShortcut("--bool"), ArgShortcut("-b")] + public bool BoolOption { get; set; } + + public void Main() + { + } +} \ No newline at end of file diff --git a/sandbox/CliFrameworkBenchmark/Commands/SystemCommandLineCommand.cs b/sandbox/CliFrameworkBenchmark/Commands/SystemCommandLineCommand.cs new file mode 100644 index 0000000..53964e6 --- /dev/null +++ b/sandbox/CliFrameworkBenchmark/Commands/SystemCommandLineCommand.cs @@ -0,0 +1,32 @@ +using System.CommandLine; +using System.CommandLine.Invocation; + +namespace Cocona.Benchmark.External.Commands; + +public class SystemCommandLineCommand +{ + public static int ExecuteHandler(string s, int i, bool b) => 0; + + public Task ExecuteAsync(string[] args) + { + var command = new RootCommand + { + new Option(new[] {"--str", "-s"}) + { + Argument = new Argument() + }, + new Option(new[] {"--int", "-i"}) + { + Argument = new Argument() + }, + new Option(new[] {"--bool", "-b"}) + { + Argument = new Argument() + } + }; + + command.Handler = CommandHandler.Create(typeof(SystemCommandLineCommand).GetMethod(nameof(ExecuteHandler))); + + return command.InvokeAsync(args); + } +} \ No newline at end of file diff --git a/sandbox/CliFrameworkBenchmark/Program.cs b/sandbox/CliFrameworkBenchmark/Program.cs new file mode 100644 index 0000000..cadcf85 --- /dev/null +++ b/sandbox/CliFrameworkBenchmark/Program.cs @@ -0,0 +1,18 @@ +// This benchmark project is based on CliFx.Benchmarks. +// https://github.com/Tyrrrz/CliFx/tree/master/CliFx.Benchmarks/ + +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Running; + +namespace Cocona.Benchmark.External; + +class Program +{ + static void Main(string[] args) + { +#pragma warning disable CS0618 + BenchmarkRunner.Run(typeof(Program).Assembly, + DefaultConfig.Instance.With(ConfigOptions.DisableOptimizationsValidator)); +#pragma warning restore CS0618 + } +} diff --git a/sandbox/CliFrameworkBenchmark/README.md b/sandbox/CliFrameworkBenchmark/README.md new file mode 100644 index 0000000..9c8ed35 --- /dev/null +++ b/sandbox/CliFrameworkBenchmark/README.md @@ -0,0 +1,5 @@ +This benchmark project is based on CliFx.Benchmarks and Cocona Benchmarks. + +https://github.com/Tyrrrz/CliFx/tree/master/CliFx.Benchmarks/ + +https://github.com/mayuki/Cocona/tree/master/perf/Cocona.Benchmark.External \ No newline at end of file diff --git a/sandbox/GeneratorSandbox/GeneratorSandbox.csproj b/sandbox/GeneratorSandbox/GeneratorSandbox.csproj new file mode 100644 index 0000000..261a5fd --- /dev/null +++ b/sandbox/GeneratorSandbox/GeneratorSandbox.csproj @@ -0,0 +1,17 @@ + + + + Exe + net8.0 + enable + enable + + + + + Analyzer + false + + + + diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs new file mode 100644 index 0000000..f234664 --- /dev/null +++ b/sandbox/GeneratorSandbox/Program.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Text.Json; +using System.Threading.Tasks; +using ConsoleAppFramework; +using Takoyaki; + +args = ["--x", "10"]; // test. + + +// var s = "foo"; +// s.AsSpan().Split(',',). + + +// BigInteger.TryParse( +// Version.TryParse( + +// Uri.TryCreate(UriCreationOptions + + +// IParsable.TryParse( + +// --x + + + +// description +// + +ConsoleApp.Run(args, static (int x) => +{ + Console.WriteLine("yah:" + x); +}); + +namespace Takoyaki +{ + public enum MyEnum + { + + } +} + + + +public class MyClass +{ + /// --tako, -t, foo bar baz. + public void Foo(int takoyaki, int y) + { + } +} + + +public interface IParser +{ + static abstract bool TryParse([NotNullWhen(true)] string? s, [MaybeNullWhen(false)] out T result); +} + + + + + +public readonly struct Vector3Parser : IParser +{ + public static bool TryParse([NotNullWhen(true)] string? s, [MaybeNullWhen(false)] out Vector3 result) + { + Span ranges = stackalloc Range[3]; + var splitCount = s.AsSpan().Split(ranges, ','); + if (splitCount != 3) + { + result = default; + return false; + } + + float x; + float y; + float z; + if (float.TryParse(s.AsSpan(ranges[0]), out x) && float.TryParse(s.AsSpan(ranges[1]), out y) && float.TryParse(s.AsSpan(ranges[2]), out z)) + { + result = new Vector3(x, y, z); + return true; + } + + result = default; + return false; + } +} + +namespace ConsoleAppFramework +{ + //[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] + //public class CommandParameterAttribute : Attribute + //{ + // public Type ParserType { get; } + + // public CommandParameterAttribute(Type parserType) + // { + // this.ParserType = parserType; + // } + //} + + //public interface IParser + //{ + // static abstract bool TryParse([NotNullWhen(true)] string? s, [MaybeNullWhen(false)] out T result); + //} + + + + + + //internal static partial class ConsoleAqpp + //{ + // public static void Run2(string[] args, Action command) + // { + // var arg0 = default(int); + // var arg0Parsed = false; + // var arg1 = default(global::MyClass); + // var arg1Parsed = false; + + // for (int i = 0; i < args.Length; i++) + // { + // var name = args[i]; + + // switch (name) + // { + // case "xxxx": + // if (!int.TryParse(args[++i], out arg0)) ThrowArgumentParseFailed("xxxx", args[i]); + // arg0Parsed = true; + // break; + // case "zzz": + // try { arg1 = System.Text.Json.JsonSerializer.Deserialize(args[++i]); } catch { ThrowArgumentParseFailed("zzz", args[i]); } + // arg1Parsed = true; + // break; + + // default: + // if (string.Equals(name, "xxxx", StringComparison.OrdinalIgnoreCase)) + // { + // if (!int.TryParse(args[++i], out arg0)) ThrowArgumentParseFailed("xxxx", args[i]); + // arg0Parsed = true; + // break; + // } + // if (string.Equals(name, "zzz", StringComparison.OrdinalIgnoreCase)) + // { + // try { arg1 = System.Text.Json.JsonSerializer.Deserialize(args[++i]); } catch { ThrowArgumentParseFailed("zzz", args[i]); } + // arg1Parsed = true; + // break; + // } + + // ThrowInvalidArgumentName(name); + // break; + // } + // } + + // if (!arg0Parsed) ThrowRequiredArgumentNotParsed("xxxx"); + // if (!arg1Parsed) ThrowRequiredArgumentNotParsed("zzz"); + + // command(arg0!, arg1!); + // } + //} +} \ No newline at end of file diff --git a/sandbox/MultiContainedApp/MultiContainedApp.csproj b/sandbox/MultiContainedApp/MultiContainedApp.csproj index 07bb775..2c0d0a8 100644 --- a/sandbox/MultiContainedApp/MultiContainedApp.csproj +++ b/sandbox/MultiContainedApp/MultiContainedApp.csproj @@ -2,7 +2,7 @@ Exe - net5.0 + net8.0 7.3 false diff --git a/sandbox/SingleContainedApp/SingleContainedApp.csproj b/sandbox/SingleContainedApp/SingleContainedApp.csproj index 7cf65c6..ddca127 100644 --- a/sandbox/SingleContainedApp/SingleContainedApp.csproj +++ b/sandbox/SingleContainedApp/SingleContainedApp.csproj @@ -2,7 +2,7 @@ Exe - net5.0 + net8.0 7.3 false diff --git a/sandbox/SingleContainedAppWithConfig/SingleContainedAppWithConfig.csproj b/sandbox/SingleContainedAppWithConfig/SingleContainedAppWithConfig.csproj index 0ce567c..e863f1c 100644 --- a/sandbox/SingleContainedAppWithConfig/SingleContainedAppWithConfig.csproj +++ b/sandbox/SingleContainedAppWithConfig/SingleContainedAppWithConfig.csproj @@ -1,36 +1,36 @@ - - Exe - net5.0 - 7.3 - false - + + Exe + net8.0 + 7.3 + false + - - - - - - + + + + + + - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + - - - + + + diff --git a/src/ConsoleAppFramework5/Command.cs b/src/ConsoleAppFramework5/Command.cs new file mode 100644 index 0000000..a159d1e --- /dev/null +++ b/src/ConsoleAppFramework5/Command.cs @@ -0,0 +1,153 @@ +using Microsoft.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; + +namespace ConsoleAppFramework; + +public record class Command +{ + public required bool IsAsync { get; init; } // Task or Task + public required bool IsVoid { get; init; } // void or int + public required bool IsRootCommand { get; init; } + public required string CommandName { get; set; } + public required CommandParameter[] Parameters { get; init; } + + public string BuildDelegateSignature() + { + if (IsAsync) + { + if (IsVoid) + { + // Func<...,Task> + } + else + { + // Func<...,Task> + } + // TODO: not yet. + throw new NotImplementedException(); + } + else + { + if (IsVoid) + { + // Action + if (Parameters.Length == 0) + { + return "Action"; + } + else + { + var parameters = string.Join(", ", Parameters.Select(x => x.Type.ToFullyQualifiedFormatDisplayString())); + return $"Action<{parameters}>"; + } + } + else + { + // Func + if (Parameters.Length == 0) + { + return "Func"; + } + else + { + var parameters = string.Join(", ", Parameters.Select(x => x.Type.ToFullyQualifiedFormatDisplayString())); + return $"Func<{parameters}, int>"; + } + } + } + } +} + +public record class CommandParameter +{ + public required ITypeSymbol Type { get; init; } + public required string Name { get; init; } + public required bool HasDefaultValue { get; init; } + public object? DefaultValue { get; init; } + public required ITypeSymbol? CustomParserType { get; init; } + + public string BuildParseMethod(int argCount, string argumentName, WellKnownTypes wellKnownTypes) + { + if (CustomParserType != null) + { + return $"if (!{CustomParserType.ToFullyQualifiedFormatDisplayString()}.TryParse(args[++i], out arg{argCount})) ThrowArgumentParseFailed(\"{argumentName}\", args[i]);"; + } + + var tryParseKnownPrimitive = false; + var tryParseIParsable = false; + + switch (Type.SpecialType) + { + case SpecialType.System_String: + return $"arg{argCount} = args[++i];"; // no parse + case SpecialType.System_Boolean: + return $"arg{argCount} = true;"; // bool is true flag + case SpecialType.System_Char: + case SpecialType.System_SByte: + case SpecialType.System_Byte: + case SpecialType.System_Int16: + case SpecialType.System_UInt16: + case SpecialType.System_Int32: + case SpecialType.System_UInt32: + case SpecialType.System_Int64: + case SpecialType.System_UInt64: + case SpecialType.System_Decimal: + case SpecialType.System_Single: + case SpecialType.System_Double: + case SpecialType.System_DateTime: + tryParseKnownPrimitive = true; + break; + default: + // Enum + if (Type.TypeKind == TypeKind.Enum) + { + return $"if (!Enum.TryParse<{Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>(args[++i], true, out arg{argCount})) ThrowArgumentParseFailed(\"{argumentName}\", args[i]);"; + } + + // System.DateTimeOffset, System.Guid, System.Version + tryParseKnownPrimitive = wellKnownTypes.HasTryParse(Type); + + if (!tryParseKnownPrimitive) + { + // IParsable (BigInteger, Complex, Half, Int128, etc...) + var parsable = wellKnownTypes.IParsable; + if (parsable != null) // has parsable + { + tryParseIParsable = Type.AllInterfaces.Any(x => x.EqualsUnconstructedGenericType(parsable)); + } + } + + break; + } + + if (tryParseKnownPrimitive) + { + return $"if (!{Type.ToFullyQualifiedFormatDisplayString()}.TryParse(args[++i], out arg{argCount})) ThrowArgumentParseFailed(\"{argumentName}\", args[i]);"; + } + else if (tryParseIParsable) + { + return $"if (!{Type.ToFullyQualifiedFormatDisplayString()}.TryParse(args[++i], null, out arg{argCount})) ThrowArgumentParseFailed(\"{argumentName}\", args[i]);"; + } + else + { + return $"try {{ arg{argCount} = System.Text.Json.JsonSerializer.Deserialize<{Type.ToFullyQualifiedFormatDisplayString()}>(args[++i]); }} catch {{ ThrowArgumentParseFailed(\"{argumentName}\", args[i]); }}"; + } + } + + public string DefaultValueToString() + { + if (DefaultValue is bool b) + { + return b ? "true" : "false"; + } + if (DefaultValue is string s) + { + return "\"" + s + "\""; + } + if (DefaultValue == null) + { + return $"({Type.ToFullyQualifiedFormatDisplayString()})null"; + } + return $"({Type.ToFullyQualifiedFormatDisplayString()}){DefaultValue}"; + } +} \ No newline at end of file diff --git a/src/ConsoleAppFramework5/ConsoleAppFramework5.csproj b/src/ConsoleAppFramework5/ConsoleAppFramework5.csproj new file mode 100644 index 0000000..d219b7e --- /dev/null +++ b/src/ConsoleAppFramework5/ConsoleAppFramework5.csproj @@ -0,0 +1,22 @@ + + + + netstandard2.0 + 12 + enable + enable + ConsoleAppFramework + + true + cs + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs new file mode 100644 index 0000000..e27c870 --- /dev/null +++ b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs @@ -0,0 +1,143 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace ConsoleAppFramework; + +[Generator(LanguageNames.CSharp)] +public partial class ConsoleAppGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterPostInitializationOutput(EmitConsoleAppTemplateSource); + + var source = context.SyntaxProvider.CreateSyntaxProvider((node, ct) => + { + if (node.IsKind(SyntaxKind.InvocationExpression)) + { + var invocationExpression = (node as InvocationExpressionSyntax); + if (invocationExpression == null) return false; + + var expr = invocationExpression.Expression as MemberAccessExpressionSyntax; + if ((expr?.Expression as IdentifierNameSyntax)?.Identifier.Text == "ConsoleApp") + { + var methodName = expr?.Name.Identifier.Text; + if (methodName is "Run" or "RunAsync") + { + return true; + } + } + + return false; + } + + return false; + }, (context, ct) => ((InvocationExpressionSyntax)context.Node, context.SemanticModel)); + + context.RegisterSourceOutput(source, EmitConsoleAppRun); + } + + static void EmitConsoleAppTemplateSource(IncrementalGeneratorPostInitializationContext context) + { + context.AddSource("ConsoleApp.cs", """ +namespace ConsoleAppFramework; + +using System; +using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; + +[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] +internal sealed class ParserAttribute : Attribute +{ +} + +internal interface IParser +{ + static abstract bool TryParse([NotNullWhen(true)] string? s, [MaybeNullWhen(false)] out T result); +} + +[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] +internal sealed class OptionAttribute : Attribute +{ + public string[] Aliases { get; } + public string? Name { get; set; } + public string? Description { get; set; } + + public OptionAttribute(params string[] aliases) + { + this.Aliases = aliases; + } +} + +internal static partial class ConsoleApp +{ + public static void Run(string[] args) + { + } + + public static Task RunAsync(string[] args) + { + return Task.CompletedTask; + } + + static void ThrowArgumentParseFailed(string argumentName, string value) + { + throw new ArgumentException($"Argument '{argumentName}' parse failed. value: {value}"); + } + + static void ThrowInvalidArgumentName(string name) + { + throw new ArgumentException($"Required argument '{name}' does not matched."); + } + + static void ThrowRequiredArgumentNotParsed(string name) + { + throw new ArgumentException($"Require argument '{name}' does not parsed."); + } + + static System.Collections.Generic.IEnumerable<(int start, int end)> Split(string str) + { + var start = 0; + for (var i = 0; i < str.Length; i++) + { + if (str[i] == ',') + { + yield return (start, i - 1); + start = i + 1; + } + } + } +} +"""); + } + + static void EmitConsoleAppRun(SourceProductionContext sourceProductionContext, (InvocationExpressionSyntax, SemanticModel) generatorSyntaxContext) + { + var node = generatorSyntaxContext.Item1; + var model = generatorSyntaxContext.Item2; + + var wellKnownTypes = new WellKnownTypes(model.Compilation); + + var parser = new Parser(sourceProductionContext, node, model); + var command = parser.ParseAndValidate(); + if (command == null) + { + return; + } + + var emitter = new Emitter(sourceProductionContext, command, wellKnownTypes); + var code = emitter.Emit(); + + sourceProductionContext.AddSource("ConsoleApp.Run.cs", $$""" +namespace ConsoleAppFramework; + +using System; +using System.Threading.Tasks; + +internal static partial class ConsoleApp +{ +{{code}} +} +"""); + } +} \ No newline at end of file diff --git a/src/ConsoleAppFramework5/Emitter.cs b/src/ConsoleAppFramework5/Emitter.cs new file mode 100644 index 0000000..cbca195 --- /dev/null +++ b/src/ConsoleAppFramework5/Emitter.cs @@ -0,0 +1,95 @@ +using Microsoft.CodeAnalysis; +using System.Text; + +namespace ConsoleAppFramework; + +internal class Emitter(SourceProductionContext context, Command command, WellKnownTypes wellKnownTypes) +{ + public string Emit() + { + // prepare argument -> + // parse argument -> + // validate parsed -> + // execute + + var prepareArgument = new StringBuilder(); + for (var i = 0; i < command.Parameters.Length; i++) + { + var parameter = command.Parameters[i]; + var defaultValue = parameter.HasDefaultValue ? parameter.DefaultValueToString() : $"default({parameter.Type.ToFullyQualifiedFormatDisplayString()})"; + prepareArgument.AppendLine($" var arg{i} = {defaultValue};"); + if (!parameter.HasDefaultValue) + { + prepareArgument.AppendLine($" var arg{i}Parsed = false;"); + } + } + + var fastParseCase = new StringBuilder(); + for (int i = 0; i < command.Parameters.Length; i++) + { + var parameter = command.Parameters[i]; + fastParseCase.AppendLine($" case \"--{parameter.Name}\":"); + fastParseCase.AppendLine($" {parameter.BuildParseMethod(i, parameter.Name, wellKnownTypes)}"); + if (!parameter.HasDefaultValue) + { + fastParseCase.AppendLine($" arg{i}Parsed = true;"); + } + fastParseCase.AppendLine(" break;"); + } + + var slowIgnoreCaseParse = new StringBuilder(); + for (int i = 0; i < command.Parameters.Length; i++) + { + var parameter = command.Parameters[i]; + slowIgnoreCaseParse.AppendLine($" if (string.Equals(name, \"--{parameter.Name}\", StringComparison.OrdinalIgnoreCase))"); + slowIgnoreCaseParse.AppendLine(" {"); + slowIgnoreCaseParse.AppendLine($" {parameter.BuildParseMethod(i, parameter.Name, wellKnownTypes)}"); + if (!parameter.HasDefaultValue) + { + slowIgnoreCaseParse.AppendLine($" arg{i}Parsed = true;"); + } + slowIgnoreCaseParse.AppendLine($" break;"); + slowIgnoreCaseParse.AppendLine(" }"); + } + + var validateParsed = new StringBuilder(); + for (int i = 0; i < command.Parameters.Length; i++) + { + var parameter = command.Parameters[i]; + if (!parameter.HasDefaultValue) + { + validateParsed.AppendLine($" if (!arg{i}Parsed) ThrowRequiredArgumentNotParsed(\"{parameter.Name}\");"); + } + } + + var methodArguments = string.Join(", ", command.Parameters.Select((x, i) => $"arg{i}!")); + + // TODO: Run or RunAsync + // TODO: void or int and handle it. + // isASync, need GetAwaiter().GetResult(); + var code = $$""" + public static void Run(string[] args, {{command.BuildDelegateSignature()}} command) + { +{{prepareArgument}} + for (int i = 0; i < args.Length; i++) + { + var name = args[i]; + + switch (name) + { +{{fastParseCase}} + default: +{{slowIgnoreCaseParse}} + ThrowInvalidArgumentName(name); + break; + } + } + +{{validateParsed}} + command({{methodArguments}}); + } +"""; + + return code; + } +} diff --git a/src/ConsoleAppFramework5/Parser.cs b/src/ConsoleAppFramework5/Parser.cs new file mode 100644 index 0000000..e6a572c --- /dev/null +++ b/src/ConsoleAppFramework5/Parser.cs @@ -0,0 +1,145 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace ConsoleAppFramework; + +internal class Parser(SourceProductionContext context, InvocationExpressionSyntax node, SemanticModel model) +{ + public Command? ParseAndValidate() + { + var args = node.ArgumentList.Arguments; + if (args.Count == 2) // 0 = args, 1 = lambda + { + var lambda = args[1].Expression as ParenthesizedLambdaExpressionSyntax; + if (lambda == null) + { + // TODO: validation(ReportDiagnostic) + return null; + } + + if (!lambda.Modifiers.Any(x => x.IsKind(SyntaxKind.StaticKeyword))) + { + // TODO: validation(need static) + return null; + } + + // TODO: check return type + + var isAsync = lambda.AsyncKeyword.IsKind(SyntaxKind.AsyncKeyword); + + var isVoid = lambda.ReturnType == null; + if (!isVoid) + { + if (!isAsync) + { + var keyword = (lambda.ReturnType as PredefinedTypeSyntax)?.Keyword; + if (keyword != null && keyword.Value.IsKind(SyntaxKind.VoidKeyword)) + { + isVoid = true; + } + else if (keyword != null && keyword.Value.IsKind(SyntaxKind.IntKeyword)) + { + isVoid = false; // ok + } + else + { + // others, invalid. + // TODO: validation invalid. + } + } + else + { + var firstType = (lambda.ReturnType as GenericNameSyntax)?.TypeArgumentList.Arguments.FirstOrDefault(); + if (firstType == null) + { + isVoid = true; // strictly, should check ret-type is Task... + } + else if ((firstType as PredefinedTypeSyntax)?.Keyword.IsKind(SyntaxKind.IntKeyword) ?? false) + { + isVoid = false; + } + else + { + // TODO: validation invalid + } + } + } + + var parameters = lambda.ParameterList.Parameters + .Where(x => x.Type != null) + .Select(x => + { + var type = model.GetTypeInfo(x.Type!); + + var hasDefault = x.Default != null; + object? defaultValue = null; + if (x.Default?.Value is LiteralExpressionSyntax literal) + { + var token = literal.Token; + defaultValue = token.Value; + } + + // bool is always optional flag + if (type.Type?.SpecialType == SpecialType.System_Boolean) + { + hasDefault = true; + defaultValue = false; + } + + var commandAttr = x.AttributeLists.SelectMany(x => x.Attributes) + .FirstOrDefault(x => + { + var name = x.Name; + if (x.Name is QualifiedNameSyntax qns) + { + name = qns.Right; + } + + var identifier = (name as GenericNameSyntax)?.Identifier; + return identifier?.ValueText is "Parser" or "ParserAttribute"; + }); + + ITypeSymbol? customParserType = null; + if (commandAttr != null) + { + var name = commandAttr.Name; + if (commandAttr.Name is QualifiedNameSyntax qns) + { + name = qns.Right; + } + var parserType = (name as GenericNameSyntax)?.TypeArgumentList.Arguments[0]; + if (parserType != null) + { + customParserType = model.GetTypeInfo(parserType).Type; + } + // TODO: validation, Type is IParsable? + } + + return new CommandParameter + { + Name = x.Identifier.Text, + Type = type.Type!, + HasDefaultValue = hasDefault, + DefaultValue = defaultValue, + CustomParserType = customParserType, + }; + }) + .Where(x => x.Type != null) + .ToArray(); + + var cmd = new Command + { + CommandName = "", + IsAsync = isAsync, + IsRootCommand = true, + IsVoid = isVoid, + Parameters = parameters + }; + + return cmd; + } + + return null; + } +} diff --git a/src/ConsoleAppFramework5/Properties/launchSettings.json b/src/ConsoleAppFramework5/Properties/launchSettings.json new file mode 100644 index 0000000..7f7a5b0 --- /dev/null +++ b/src/ConsoleAppFramework5/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Roslyn Debug Profile": { + "commandName": "DebugRoslynComponent", + "targetProject": "..\\..\\sandbox\\GeneratorSandbox\\GeneratorSandbox.csproj" + } + } +} \ No newline at end of file diff --git a/src/ConsoleAppFramework5/RoslynExtensions.cs b/src/ConsoleAppFramework5/RoslynExtensions.cs new file mode 100644 index 0000000..79a6b8d --- /dev/null +++ b/src/ConsoleAppFramework5/RoslynExtensions.cs @@ -0,0 +1,18 @@ +using Microsoft.CodeAnalysis; + +namespace ConsoleAppFramework; + +internal static class RoslynExtensions +{ + internal static string ToFullyQualifiedFormatDisplayString(this ITypeSymbol typeSymbol) + { + return typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + } + + public static bool EqualsUnconstructedGenericType(this INamedTypeSymbol left, INamedTypeSymbol right) + { + var l = left.IsGenericType ? left.ConstructUnboundGenericType() : left; + var r = right.IsGenericType ? right.ConstructUnboundGenericType() : right; + return SymbolEqualityComparer.Default.Equals(l, r); + } +} diff --git a/src/ConsoleAppFramework5/WellKnownTypes.cs b/src/ConsoleAppFramework5/WellKnownTypes.cs new file mode 100644 index 0000000..9446c66 --- /dev/null +++ b/src/ConsoleAppFramework5/WellKnownTypes.cs @@ -0,0 +1,40 @@ +using Microsoft.CodeAnalysis; + +namespace ConsoleAppFramework; + +public class WellKnownTypes(Compilation compilation) +{ + INamedTypeSymbol? dateTimeOffset; + public INamedTypeSymbol DateTimeOffset => dateTimeOffset ??= GetTypeByMetadataName("System.DateTimeOffset"); + + INamedTypeSymbol? guid; + public INamedTypeSymbol Guid => guid ??= GetTypeByMetadataName("System.Guid"); + + INamedTypeSymbol? version; + public INamedTypeSymbol Version => version ??= GetTypeByMetadataName("System.Version"); + + INamedTypeSymbol? parsable; + public INamedTypeSymbol? IParsable => parsable ??= compilation.GetTypeByMetadataName("System.IParsable`1"); + + public bool HasTryParse(ITypeSymbol type) + { + if (SymbolEqualityComparer.Default.Equals(type, DateTimeOffset) + || SymbolEqualityComparer.Default.Equals(type, Guid) + || SymbolEqualityComparer.Default.Equals(type, Version) + ) + { + return true; + } + return false; + } + + INamedTypeSymbol GetTypeByMetadataName(string metadataName) + { + var symbol = compilation.GetTypeByMetadataName(metadataName); + if (symbol == null) + { + throw new InvalidOperationException($"Type {metadataName} is not found in compilation."); + } + return symbol; + } +} From 701601c0ccb8544f0d42a2918c3f85a927af11f7 Mon Sep 17 00:00:00 2001 From: neuecc Date: Mon, 13 May 2024 18:24:07 +0900 Subject: [PATCH 02/54] impl iroiro many --- ConsoleAppFramework.sln | 11 +- sandbox/CliFrameworkBenchmark/Benchmark.cs | 81 +- .../CliFrameworkBenchmark.csproj | 15 +- .../Commands/ConsoleAppFrameworkCommand.cs | 28 +- sandbox/CliFrameworkBenchmark/Program.cs | 5 +- .../GeneratorSandbox/GeneratorSandbox.csproj | 5 + sandbox/GeneratorSandbox/Program.cs | 148 ++- src/ConsoleAppFramework/ConsoleAppEngine.cs | 1024 ++++++++--------- src/ConsoleAppFramework5/Command.cs | 91 +- .../ConsoleAppGenerator.cs | 61 +- src/ConsoleAppFramework5/Emitter.cs | 115 +- src/ConsoleAppFramework5/Parser.cs | 308 +++-- src/ConsoleAppFramework5/WellKnownTypes.cs | 3 + 13 files changed, 1123 insertions(+), 772 deletions(-) diff --git a/ConsoleAppFramework.sln b/ConsoleAppFramework.sln index 5bbdbb3..deb796d 100644 --- a/ConsoleAppFramework.sln +++ b/ConsoleAppFramework.sln @@ -32,9 +32,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Net6WebApp", "sandbox\Net6W EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Net6Console", "sandbox\Net6Console\Net6Console.csproj", "{19E33348-979A-4283-A74D-0844CC384A88}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleAppFramework5", "src\ConsoleAppFramework5\ConsoleAppFramework5.csproj", "{09BEEA7B-B6D3-4011-BCAB-6DF976713695}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleAppFramework5", "src\ConsoleAppFramework5\ConsoleAppFramework5.csproj", "{09BEEA7B-B6D3-4011-BCAB-6DF976713695}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GeneratorSandbox", "sandbox\GeneratorSandbox\GeneratorSandbox.csproj", "{ACDA48BA-0BFE-4917-B335-7836DAA5929A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GeneratorSandbox", "sandbox\GeneratorSandbox\GeneratorSandbox.csproj", "{ACDA48BA-0BFE-4917-B335-7836DAA5929A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFrameworkBenchmark", "sandbox\CliFrameworkBenchmark\CliFrameworkBenchmark.csproj", "{F558E4F2-1AB0-4634-B613-69DFE79894AF}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -78,6 +80,10 @@ Global {ACDA48BA-0BFE-4917-B335-7836DAA5929A}.Debug|Any CPU.Build.0 = Debug|Any CPU {ACDA48BA-0BFE-4917-B335-7836DAA5929A}.Release|Any CPU.ActiveCfg = Release|Any CPU {ACDA48BA-0BFE-4917-B335-7836DAA5929A}.Release|Any CPU.Build.0 = Release|Any CPU + {F558E4F2-1AB0-4634-B613-69DFE79894AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F558E4F2-1AB0-4634-B613-69DFE79894AF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F558E4F2-1AB0-4634-B613-69DFE79894AF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F558E4F2-1AB0-4634-B613-69DFE79894AF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -92,6 +98,7 @@ Global {19E33348-979A-4283-A74D-0844CC384A88} = {A2CF2984-E8E2-48FC-B5A1-58D74A2467E6} {09BEEA7B-B6D3-4011-BCAB-6DF976713695} = {1F399F98-7439-4F05-847B-CC1267B4B7F2} {ACDA48BA-0BFE-4917-B335-7836DAA5929A} = {A2CF2984-E8E2-48FC-B5A1-58D74A2467E6} + {F558E4F2-1AB0-4634-B613-69DFE79894AF} = {A2CF2984-E8E2-48FC-B5A1-58D74A2467E6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7F3E353A-C125-4020-8481-11DC6496358C} diff --git a/sandbox/CliFrameworkBenchmark/Benchmark.cs b/sandbox/CliFrameworkBenchmark/Benchmark.cs index 379be2a..b04fb73 100644 --- a/sandbox/CliFrameworkBenchmark/Benchmark.cs +++ b/sandbox/CliFrameworkBenchmark/Benchmark.cs @@ -7,52 +7,71 @@ using Cocona.Benchmark.External.Commands; using CommandLine; using ConsoleAppFramework; -using Microsoft.Extensions.Hosting; namespace Cocona.Benchmark.External; -[SimpleJob] +// [SimpleJob] +[ShortRunJob] [RankColumn] +[MemoryDiagnoser] [Orderer(SummaryOrderPolicy.FastestToSlowest)] public class Benchmark { private static readonly string[] Arguments = { "--str", "hello world", "-i", "13", "-b" }; + private static readonly string[] Arguments2 = { "--str", "hello world", "--i", "13", "--b" }; - [Benchmark(Description = "Cocona.Lite", Baseline = true)] - public async Task ExecuteWithCoconaLite() => - await Cocona.CoconaLiteApp.RunAsync(Arguments); + //[Benchmark(Description = "Cocona.Lite", Baseline = true)] + //public async Task ExecuteWithCoconaLite() => + // await Cocona.CoconaLiteApp.RunAsync(Arguments); - [Benchmark(Description = "Cocona")] - public async ValueTask ExecuteWithCocona() => - await Cocona.CoconaApp.RunAsync(Arguments); + //[Benchmark(Description = "Cocona")] + //public async ValueTask ExecuteWithCocona() => + // await Cocona.CoconaApp.RunAsync(Arguments); - [Benchmark(Description = "ConsoleAppFramework")] - public async ValueTask ExecuteWithConsoleAppFramework() => - await Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(Arguments); + ////[Benchmark(Description = "ConsoleAppFramework")] + ////public async ValueTask ExecuteWithConsoleAppFramework() => + //// await Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(Arguments); - [Benchmark(Description = "CliFx")] - public async ValueTask ExecuteWithCliFx() => - await new CliApplicationBuilder().AddCommand(typeof(CliFxCommand)).Build().RunAsync(Arguments); + //[Benchmark(Description = "CliFx")] + //public async ValueTask ExecuteWithCliFx() => + // await new CliApplicationBuilder().AddCommand(typeof(CliFxCommand)).Build().RunAsync(Arguments); - [Benchmark(Description = "System.CommandLine")] - public async Task ExecuteWithSystemCommandLine() => - await new SystemCommandLineCommand().ExecuteAsync(Arguments); + //[Benchmark(Description = "System.CommandLine")] + //public async Task ExecuteWithSystemCommandLine() => + // await new SystemCommandLineCommand().ExecuteAsync(Arguments); - [Benchmark(Description = "McMaster.Extensions.CommandLineUtils")] - public int ExecuteWithMcMaster() => - McMaster.Extensions.CommandLineUtils.CommandLineApplication.Execute(Arguments); + //[Benchmark(Description = "McMaster.Extensions.CommandLineUtils")] + //public int ExecuteWithMcMaster() => + // McMaster.Extensions.CommandLineUtils.CommandLineApplication.Execute(Arguments); - [Benchmark(Description = "CommandLineParser")] - public void ExecuteWithCommandLineParser() => - new Parser() - .ParseArguments(Arguments, typeof(CommandLineParserCommand)) - .WithParsed(c => c.Execute()); + //[Benchmark(Description = "CommandLineParser")] + //public void ExecuteWithCommandLineParser() => + // new Parser() + // .ParseArguments(Arguments, typeof(CommandLineParserCommand)) + // .WithParsed(c => c.Execute()); - [Benchmark(Description = "PowerArgs")] - public void ExecuteWithPowerArgs() => - PowerArgs.Args.InvokeMain(Arguments); + //[Benchmark(Description = "PowerArgs")] + //public void ExecuteWithPowerArgs() => + // PowerArgs.Args.InvokeMain(Arguments); - [Benchmark(Description = "Clipr")] - public void ExecuteWithClipr() => - clipr.CliParser.Parse(Arguments).Execute(); + //[Benchmark(Description = "Clipr")] + //public void ExecuteWithClipr() => + // clipr.CliParser.Parse(Arguments).Execute(); + + + [Benchmark(Description = "ConsoleAppFramework v5")] + public void ExecuteConsoleAppFramework5() + { + ConsoleApp.Run(Arguments2, static (string str, int i, bool b) => { }); + } + + //[Benchmark(Description = "ConsoleAppFramework v5(FP)")] + //public unsafe void ExecuteConsoleAppFramework5_2() + //{ + // ConsoleApp.Run(Arguments2, &Run); + + // static void Run(string str, int i, bool b) + // { + // } + //} } \ No newline at end of file diff --git a/sandbox/CliFrameworkBenchmark/CliFrameworkBenchmark.csproj b/sandbox/CliFrameworkBenchmark/CliFrameworkBenchmark.csproj index 59d9ef9..a744b05 100644 --- a/sandbox/CliFrameworkBenchmark/CliFrameworkBenchmark.csproj +++ b/sandbox/CliFrameworkBenchmark/CliFrameworkBenchmark.csproj @@ -5,19 +5,28 @@ net8.0 enable annotations + true - + - - + + + + + + Analyzer + false + + + diff --git a/sandbox/CliFrameworkBenchmark/Commands/ConsoleAppFrameworkCommand.cs b/sandbox/CliFrameworkBenchmark/Commands/ConsoleAppFrameworkCommand.cs index b85275a..184bb68 100644 --- a/sandbox/CliFrameworkBenchmark/Commands/ConsoleAppFrameworkCommand.cs +++ b/sandbox/CliFrameworkBenchmark/Commands/ConsoleAppFrameworkCommand.cs @@ -1,16 +1,16 @@ -using ConsoleAppFramework; +//using ConsoleAppFramework; -namespace Cocona.Benchmark.External.Commands; +//namespace Cocona.Benchmark.External.Commands; -public class ConsoleAppFrameworkCommand : ConsoleAppBase -{ - public void Execute( - [global::ConsoleAppFramework.Option("s")] - string? str, - [global::ConsoleAppFramework.Option("i")] - int intOption, - [global::ConsoleAppFramework.Option("b")] - bool boolOption) - { - } -} \ No newline at end of file +//public class ConsoleAppFrameworkCommand : ConsoleAppBase +//{ +// public void Execute( +// [global::ConsoleAppFramework.Option("s")] +// string? str, +// [global::ConsoleAppFramework.Option("i")] +// int intOption, +// [global::ConsoleAppFramework.Option("b")] +// bool boolOption) +// { +// } +//} \ No newline at end of file diff --git a/sandbox/CliFrameworkBenchmark/Program.cs b/sandbox/CliFrameworkBenchmark/Program.cs index cadcf85..131ae0e 100644 --- a/sandbox/CliFrameworkBenchmark/Program.cs +++ b/sandbox/CliFrameworkBenchmark/Program.cs @@ -10,9 +10,6 @@ class Program { static void Main(string[] args) { -#pragma warning disable CS0618 - BenchmarkRunner.Run(typeof(Program).Assembly, - DefaultConfig.Instance.With(ConfigOptions.DisableOptimizationsValidator)); -#pragma warning restore CS0618 + BenchmarkRunner.Run(DefaultConfig.Instance.WithOptions(ConfigOptions.DisableOptimizationsValidator)); } } diff --git a/sandbox/GeneratorSandbox/GeneratorSandbox.csproj b/sandbox/GeneratorSandbox/GeneratorSandbox.csproj index 261a5fd..60f5cf1 100644 --- a/sandbox/GeneratorSandbox/GeneratorSandbox.csproj +++ b/sandbox/GeneratorSandbox/GeneratorSandbox.csproj @@ -5,8 +5,13 @@ net8.0 enable enable + true + + + + Analyzer diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index f234664..7fd91fd 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -2,9 +2,13 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; using ConsoleAppFramework; +using Microsoft.Extensions.DependencyInjection; using Takoyaki; args = ["--x", "10"]; // test. @@ -29,30 +33,86 @@ // description // -ConsoleApp.Run(args, static (int x) => +var sc = new ServiceCollection(); +sc.AddSingleton(); +var provider = sc.BuildServiceProvider(); +ConsoleApp.ServiceProvider = provider; + + +//var cts = new CancellationTokenSource(); + +//var iii = 0; +//while (true) +//{ +// Thread.Sleep(TimeSpan.FromSeconds(1)); +// Console.WriteLine(iii++ + ", " + cts.IsCancellationRequested); +//} + + +//delegate* managed a = &Method; + +// sp.GetService(); + +await ConsoleApp.RunAsync(args, static async (int x, [FromServices] MyClass mc, CancellationToken cancellationToken) => { - Console.WriteLine("yah:" + x); + Console.WriteLine((x, mc)); + await Task.Yield(); + await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken); + Console.WriteLine("end"); }); -namespace Takoyaki + +//ConsoleApp.Run(args, &Methods.Method); + +static void Foo(RunRun rrrr) { - public enum MyEnum +} + + +internal delegate void RunRun(int x, int y = 100); + + +public static class Methods +{ + public static void Method(int x, int y = 12345) { } } +public class MyClass +{ + +} + -public class MyClass +namespace Takoyaki { - /// --tako, -t, foo bar baz. - public void Foo(int takoyaki, int y) + public enum MyEnum { + + } + + public static class Hoge + { + public static void Nano(int x) + { + } } } + +//public class MyClass +//{ +// /// --tako, -t, foo bar baz. +// public void Foo(int takoyaki, int y) +// { +// } +//} + + public interface IParser { static abstract bool TryParse([NotNullWhen(true)] string? s, [MaybeNullWhen(false)] out T result); @@ -90,73 +150,7 @@ public static bool TryParse([NotNullWhen(true)] string? s, [MaybeNullWhen(false) namespace ConsoleAppFramework { - //[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] - //public class CommandParameterAttribute : Attribute - //{ - // public Type ParserType { get; } - - // public CommandParameterAttribute(Type parserType) - // { - // this.ParserType = parserType; - // } - //} - - //public interface IParser - //{ - // static abstract bool TryParse([NotNullWhen(true)] string? s, [MaybeNullWhen(false)] out T result); - //} - - - - - - //internal static partial class ConsoleAqpp - //{ - // public static void Run2(string[] args, Action command) - // { - // var arg0 = default(int); - // var arg0Parsed = false; - // var arg1 = default(global::MyClass); - // var arg1Parsed = false; - - // for (int i = 0; i < args.Length; i++) - // { - // var name = args[i]; - - // switch (name) - // { - // case "xxxx": - // if (!int.TryParse(args[++i], out arg0)) ThrowArgumentParseFailed("xxxx", args[i]); - // arg0Parsed = true; - // break; - // case "zzz": - // try { arg1 = System.Text.Json.JsonSerializer.Deserialize(args[++i]); } catch { ThrowArgumentParseFailed("zzz", args[i]); } - // arg1Parsed = true; - // break; - - // default: - // if (string.Equals(name, "xxxx", StringComparison.OrdinalIgnoreCase)) - // { - // if (!int.TryParse(args[++i], out arg0)) ThrowArgumentParseFailed("xxxx", args[i]); - // arg0Parsed = true; - // break; - // } - // if (string.Equals(name, "zzz", StringComparison.OrdinalIgnoreCase)) - // { - // try { arg1 = System.Text.Json.JsonSerializer.Deserialize(args[++i]); } catch { ThrowArgumentParseFailed("zzz", args[i]); } - // arg1Parsed = true; - // break; - // } - - // ThrowInvalidArgumentName(name); - // break; - // } - // } - - // if (!arg0Parsed) ThrowRequiredArgumentNotParsed("xxxx"); - // if (!arg1Parsed) ThrowRequiredArgumentNotParsed("zzz"); - - // command(arg0!, arg1!); - // } - //} -} \ No newline at end of file + + +} + diff --git a/src/ConsoleAppFramework/ConsoleAppEngine.cs b/src/ConsoleAppFramework/ConsoleAppEngine.cs index 0f6b647..cebd57d 100644 --- a/src/ConsoleAppFramework/ConsoleAppEngine.cs +++ b/src/ConsoleAppFramework/ConsoleAppEngine.cs @@ -1,518 +1,518 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Collections.ObjectModel; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Reflection; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; - -namespace ConsoleAppFramework -{ - internal class ConsoleAppEngine - { - readonly ILogger logger; - readonly IServiceProvider provider; - readonly CancellationTokenSource cancellationTokenSource; - readonly ConsoleAppOptions options; - readonly IServiceProviderIsService isService; - readonly IParamsValidator paramsValidator; - readonly bool isStrict; - - public ConsoleAppEngine(ILogger logger, - IServiceProvider provider, - ConsoleAppOptions options, - IServiceProviderIsService isService, - IParamsValidator paramsValidator, - CancellationTokenSource cancellationTokenSource) - { - this.logger = logger; - this.provider = provider; - this.paramsValidator = paramsValidator; - this.cancellationTokenSource = cancellationTokenSource; - this.options = options; - this.isService = isService; - this.isStrict = options.StrictOption; - } - - public async Task RunAsync() - { - logger.LogTrace("ConsoleAppEngine.Run Start"); - - var args = options.CommandLineArguments; - - if (!options.CommandDescriptors.TryGetDescriptor(args, out var commandDescriptor, out var offset)) - { - if (args.Length == 0) - { - if (options.CommandDescriptors.TryGetHelpMethod(out commandDescriptor)) - { - goto RUN; - } - } - - // TryGet Single help or Version - if (args.Length == 1) - { - switch (args[0].Trim('-')) - { - case "help": - if (options.CommandDescriptors.TryGetHelpMethod(out commandDescriptor)) - { - goto RUN; - } - break; - case "version": - if (options.CommandDescriptors.TryGetVersionMethod(out commandDescriptor)) - { - goto RUN; - } - break; - default: - break; - } - } - - // TryGet SubCommands Help - if (args.Length >= 2 && args[1].Trim('-') == "help") - { - var subCommands = options.CommandDescriptors.GetSubCommands(args[0]); - if (subCommands.Length != 0) - { - var msg = new CommandHelpBuilder(() => args[0], isService, options).BuildHelpMessage(null, subCommands, shortCommandName: true); - Console.WriteLine(msg); - return; - } - } - - await SetFailAsync("Command not found. args: " + string.Join(" ", args)); - return; - } - - // foo --help - // foo bar --help - if (args.Skip(offset).FirstOrDefault()?.Trim('-') == "help") - { - var msg = new CommandHelpBuilder(() => commandDescriptor.GetCommandName(options), isService, options).BuildHelpMessage(commandDescriptor); - Console.WriteLine(msg); - return; - } - - // check can invoke help - if (commandDescriptor.CommandType == CommandType.DefaultCommand && args.Length == 0) - { - var p = commandDescriptor.MethodInfo.GetParameters(); - if (p.Any(x => !(x.ParameterType == typeof(ConsoleAppContext) || isService.IsService(x.ParameterType) || x.HasDefaultValue))) - { - options.CommandDescriptors.TryGetHelpMethod(out commandDescriptor); - } - } - - RUN: - await RunCore(commandDescriptor!.MethodInfo!.DeclaringType!, commandDescriptor.MethodInfo, commandDescriptor.Instance, args, offset); - } - - // Try to invoke method. - async Task RunCore(Type type, MethodInfo methodInfo, object? instance, string?[] args, int argsOffset) - { - object?[] invokeArgs; - ParameterInfo[] originalParameters = methodInfo.GetParameters(); - var isService = provider.GetService(); - try - { - var parameters = originalParameters; - if (isService != null) - { - parameters = parameters.Where(x => !(x.ParameterType == typeof(ConsoleAppContext) || isService.IsService(x.ParameterType))).ToArray(); - } - - if (!TryGetInvokeArguments(parameters, args, argsOffset, out invokeArgs, out var errorMessage)) - { - await SetFailAsync(errorMessage + " args: " + string.Join(" ", args)); - return; - } - - } - catch (Exception ex) - { - await SetFailAsync("Fail to match method parameter on " + type.Name + "." + methodInfo.Name + ". args: " + string.Join(" ", args), ex); - return; - } - - var ctx = new ConsoleAppContext(args, DateTime.UtcNow, cancellationTokenSource, logger, methodInfo, provider); - - // re:create invokeArgs, merge with DI parameter. - if (invokeArgs.Length != originalParameters.Length) - { - var newInvokeArgs = new object?[originalParameters.Length]; - var invokeArgsIndex = 0; - for (int i = 0; i < originalParameters.Length; i++) - { - var p = originalParameters[i].ParameterType; - if (p == typeof(ConsoleAppContext)) - { - newInvokeArgs[i] = ctx; - } - else if (isService!.IsService(p)) - { - try - { - newInvokeArgs[i] = provider.GetService(p); - } - catch (Exception ex) - { - await SetFailAsync("Fail to get service parameter. ParameterType:" + p.FullName, ex); - return; - } - } - else - { - newInvokeArgs[i] = invokeArgs[invokeArgsIndex++]; - } - } - invokeArgs = newInvokeArgs; - } - - var validationResult = paramsValidator.ValidateParameters(originalParameters.Zip(invokeArgs)); - if (validationResult != ValidationResult.Success) - { - await SetFailAsync(validationResult!.ErrorMessage!); - return; - } - - try - { - if (instance == null && !type.IsAbstract && !methodInfo.IsStatic) - { - instance = ActivatorUtilities.CreateInstance(provider, type); - typeof(ConsoleAppBase).GetProperty(nameof(ConsoleAppBase.Context))!.SetValue(instance, ctx); - } - - } - catch (Exception ex) - { - await SetFailAsync("Fail to create ConsoleAppBase instance. Type:" + type.FullName, ex); - return; - } - - try - { - var invoker = new WithFilterInvoker(methodInfo, instance, invokeArgs, provider, options.GlobalFilters ?? Array.Empty(), ctx); - try - { - var result = await invoker.InvokeAsync(); - if (result != null) - { - Environment.ExitCode = result.Value; - } - } - finally - { - if (instance is IAsyncDisposable ad) - { - await ad.DisposeAsync(); - } - else if (instance is IDisposable d) - { - d.Dispose(); - } - } - } - catch (Exception ex) - { - if (ex is TargetInvocationException tex) - { - ex = tex.InnerException ?? tex; - } - - if (ex is OperationCanceledException operationCanceledException && operationCanceledException.CancellationToken == cancellationTokenSource.Token) - { - // NOTE: Do nothing if the exception has thrown by the CancellationToken of ConsoleAppEngine. - // If the user code throws OperationCanceledException, ConsoleAppEngine should not handle that. - return; - } - - await SetFailAsync("Fail in application running on " + type.Name + "." + methodInfo.Name, ex); - return; - } - - logger.LogTrace("ConsoleAppEngine.Run Complete Successfully"); - } - - ValueTask SetFailAsync(string message) - { - Environment.ExitCode = 1; - logger.LogError(message); - return default; - } - - ValueTask SetFailAsync(string message, Exception? ex) - { - Environment.ExitCode = 1; - logger.LogError(ex, message); - return default; - } - - bool TryGetInvokeArguments(ParameterInfo[] parameters, string?[] args, int argsOffset, out object?[] invokeArgs, out string? errorMessage) - { - try - { - var jsonOption = options.JsonSerializerOptions; - - // Collect option types for parsing command-line arguments. - var optionTypeByOptionName = new Dictionary(StringComparer.OrdinalIgnoreCase); - for (int i = 0; i < parameters.Length; i++) - { - var item = parameters[i]; - var option = item.GetCustomAttribute(); - - optionTypeByOptionName[(isStrict ? "--" : "") + options.NameConverter(item.Name!)] = item.ParameterType; - if (!string.IsNullOrWhiteSpace(option?.ShortName)) - { - optionTypeByOptionName[(isStrict ? "-" : "") + option!.ShortName!] = item.ParameterType; - } - } - - var (argumentDictionary, optionByIndex) = ParseArgument(args, argsOffset, optionTypeByOptionName, isStrict); - invokeArgs = new object[parameters.Length]; - - for (int i = 0; i < parameters.Length; i++) - { - var item = parameters[i]; - var itemName = options.NameConverter(item.Name!); - var option = item.GetCustomAttribute(); - if (!string.IsNullOrWhiteSpace(option?.ShortName) && char.IsDigit(option!.ShortName, 0)) throw new InvalidOperationException($"Option '{itemName}' has a short name, but the short name must start with A-Z or a-z."); - - var value = default(OptionParameter); - - // Indexed arguments (e.g. [Option(0)]) - if (option != null && option.Index != -1) - { - if (optionByIndex.Count <= option.Index) - { - if (!item.HasDefaultValue) - { - throw new InvalidOperationException($"Required argument {option.Index} was not found in specified arguments."); - } - } - else - { - value = optionByIndex[option.Index]; - } - } - - // Keyed options (e.g. -foo -bar ) - var longName = (isStrict) ? ("--" + itemName) : itemName; - var shortName = (isStrict) ? ("-" + option?.ShortName?.TrimStart('-')) : option?.ShortName?.TrimStart('-'); - - if (value.Value != null || argumentDictionary.TryGetValue(longName!, out value) || argumentDictionary.TryGetValue(shortName ?? "", out value)) - { - if (parameters[i].ParameterType == typeof(bool) && value.Value == null) - { - invokeArgs[i] = value.BooleanSwitch; - continue; - } - - if (value.Value != null) - { - if (parameters[i].ParameterType == typeof(string)) - { - // when string, invoke directly(avoid JSON escape) - invokeArgs[i] = value.Value; - continue; - } - else if (parameters[i].ParameterType.IsEnum) - { - try - { - invokeArgs[i] = Enum.Parse(parameters[i].ParameterType, value.Value, true); - continue; - } - catch - { - errorMessage = "Parameter \"" + itemName + "\"" + " fail on Enum parsing."; - return false; - } - } - else if (typeof(System.Collections.IEnumerable).IsAssignableFrom(parameters[i].ParameterType) && !typeof(System.Collections.IDictionary).IsAssignableFrom(parameters[i].ParameterType)) - { - var v = value.Value; - if (!(v.StartsWith("[") && v.EndsWith("]"))) - { - var elemType = UnwrapCollectionElementType(parameters[i].ParameterType); - if (elemType == typeof(string)) - { - if (!(v.StartsWith("\"") && v.EndsWith("\""))) - { - v = "[" + string.Join(",", v.Split(' ', ',').Select(x => "\"" + x + "\"")) + "]"; - } - else - { - v = "[" + v + "]"; - } - } - else - { - v = "[" + string.Join(",", v.Trim('\'', '\"').Split(' ', ',')) + "]"; - } - } - try - { - invokeArgs[i] = JsonSerializer.Deserialize(v, parameters[i].ParameterType, jsonOption); - continue; - } - catch - { - errorMessage = "Parameter \"" + itemName + "\"" + " fail on JSON deserialize, please check type or JSON escape or add double-quotation."; - return false; - } - } - else - { - var v = value.Value; - try - { - try - { - invokeArgs[i] = JsonSerializer.Deserialize(v, parameters[i].ParameterType, jsonOption); - continue; - } - catch (JsonException) +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Collections.ObjectModel; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace ConsoleAppFramework +{ + internal class ConsoleAppEngine + { + readonly ILogger logger; + readonly IServiceProvider provider; + readonly CancellationTokenSource cancellationTokenSource; + readonly ConsoleAppOptions options; + readonly IServiceProviderIsService isService; + readonly IParamsValidator paramsValidator; + readonly bool isStrict; + + public ConsoleAppEngine(ILogger logger, + IServiceProvider provider, + ConsoleAppOptions options, + IServiceProviderIsService isService, + IParamsValidator paramsValidator, + CancellationTokenSource cancellationTokenSource) + { + this.logger = logger; + this.provider = provider; + this.paramsValidator = paramsValidator; + this.cancellationTokenSource = cancellationTokenSource; + this.options = options; + this.isService = isService; + this.isStrict = options.StrictOption; + } + + public async Task RunAsync() + { + logger.LogTrace("ConsoleAppEngine.Run Start"); + + var args = options.CommandLineArguments; + + if (!options.CommandDescriptors.TryGetDescriptor(args, out var commandDescriptor, out var offset)) + { + if (args.Length == 0) + { + if (options.CommandDescriptors.TryGetHelpMethod(out commandDescriptor)) + { + goto RUN; + } + } + + // TryGet Single help or Version + if (args.Length == 1) + { + switch (args[0].Trim('-')) + { + case "help": + if (options.CommandDescriptors.TryGetHelpMethod(out commandDescriptor)) + { + goto RUN; + } + break; + case "version": + if (options.CommandDescriptors.TryGetVersionMethod(out commandDescriptor)) + { + goto RUN; + } + break; + default: + break; + } + } + + // TryGet SubCommands Help + if (args.Length >= 2 && args[1].Trim('-') == "help") + { + var subCommands = options.CommandDescriptors.GetSubCommands(args[0]); + if (subCommands.Length != 0) + { + var msg = new CommandHelpBuilder(() => args[0], isService, options).BuildHelpMessage(null, subCommands, shortCommandName: true); + Console.WriteLine(msg); + return; + } + } + + await SetFailAsync("Command not found. args: " + string.Join(" ", args)); + return; + } + + // foo --help + // foo bar --help + if (args.Skip(offset).FirstOrDefault()?.Trim('-') == "help") + { + var msg = new CommandHelpBuilder(() => commandDescriptor.GetCommandName(options), isService, options).BuildHelpMessage(commandDescriptor); + Console.WriteLine(msg); + return; + } + + // check can invoke help + if (commandDescriptor.CommandType == CommandType.DefaultCommand && args.Length == 0) + { + var p = commandDescriptor.MethodInfo.GetParameters(); + if (p.Any(x => !(x.ParameterType == typeof(ConsoleAppContext) || isService.IsService(x.ParameterType) || x.HasDefaultValue))) + { + options.CommandDescriptors.TryGetHelpMethod(out commandDescriptor); + } + } + + RUN: + await RunCore(commandDescriptor!.MethodInfo!.DeclaringType!, commandDescriptor.MethodInfo, commandDescriptor.Instance, args, offset); + } + + // Try to invoke method. + async Task RunCore(Type type, MethodInfo methodInfo, object? instance, string?[] args, int argsOffset) + { + object?[] invokeArgs; + ParameterInfo[] originalParameters = methodInfo.GetParameters(); + var isService = provider.GetService(); + try + { + var parameters = originalParameters; + if (isService != null) + { + parameters = parameters.Where(x => !(x.ParameterType == typeof(ConsoleAppContext) || isService.IsService(x.ParameterType))).ToArray(); + } + + if (!TryGetInvokeArguments(parameters, args, argsOffset, out invokeArgs, out var errorMessage)) + { + await SetFailAsync(errorMessage + " args: " + string.Join(" ", args)); + return; + } + + } + catch (Exception ex) + { + await SetFailAsync("Fail to match method parameter on " + type.Name + "." + methodInfo.Name + ". args: " + string.Join(" ", args), ex); + return; + } + + var ctx = new ConsoleAppContext(args, DateTime.UtcNow, cancellationTokenSource, logger, methodInfo, provider); + + // re:create invokeArgs, merge with DI parameter. + if (invokeArgs.Length != originalParameters.Length) + { + var newInvokeArgs = new object?[originalParameters.Length]; + var invokeArgsIndex = 0; + for (int i = 0; i < originalParameters.Length; i++) + { + var p = originalParameters[i].ParameterType; + if (p == typeof(ConsoleAppContext)) + { + newInvokeArgs[i] = ctx; + } + else if (isService!.IsService(p)) + { + try + { + newInvokeArgs[i] = provider.GetService(p); + } + catch (Exception ex) + { + await SetFailAsync("Fail to get service parameter. ParameterType:" + p.FullName, ex); + return; + } + } + else + { + newInvokeArgs[i] = invokeArgs[invokeArgsIndex++]; + } + } + invokeArgs = newInvokeArgs; + } + + var validationResult = paramsValidator.ValidateParameters(originalParameters.Zip(invokeArgs)); + if (validationResult != ValidationResult.Success) + { + await SetFailAsync(validationResult!.ErrorMessage!); + return; + } + + try + { + if (instance == null && !type.IsAbstract && !methodInfo.IsStatic) + { + instance = ActivatorUtilities.CreateInstance(provider, type); + typeof(ConsoleAppBase).GetProperty(nameof(ConsoleAppBase.Context))!.SetValue(instance, ctx); + } + + } + catch (Exception ex) + { + await SetFailAsync("Fail to create ConsoleAppBase instance. Type:" + type.FullName, ex); + return; + } + + try + { + var invoker = new WithFilterInvoker(methodInfo, instance, invokeArgs, provider, options.GlobalFilters ?? Array.Empty(), ctx); + try + { + var result = await invoker.InvokeAsync(); + if (result != null) + { + Environment.ExitCode = result.Value; + } + } + finally + { + if (instance is IAsyncDisposable ad) + { + await ad.DisposeAsync(); + } + else if (instance is IDisposable d) + { + d.Dispose(); + } + } + } + catch (Exception ex) + { + if (ex is TargetInvocationException tex) + { + ex = tex.InnerException ?? tex; + } + + if (ex is OperationCanceledException operationCanceledException && operationCanceledException.CancellationToken == cancellationTokenSource.Token) + { + // NOTE: Do nothing if the exception has thrown by the CancellationToken of ConsoleAppEngine. + // If the user code throws OperationCanceledException, ConsoleAppEngine should not handle that. + return; + } + + await SetFailAsync("Fail in application running on " + type.Name + "." + methodInfo.Name, ex); + return; + } + + logger.LogTrace("ConsoleAppEngine.Run Complete Successfully"); + } + + ValueTask SetFailAsync(string message) + { + Environment.ExitCode = 1; + logger.LogError(message); + return default; + } + + ValueTask SetFailAsync(string message, Exception? ex) + { + Environment.ExitCode = 1; + logger.LogError(ex, message); + return default; + } + + bool TryGetInvokeArguments(ParameterInfo[] parameters, string?[] args, int argsOffset, out object?[] invokeArgs, out string? errorMessage) + { + try + { + var jsonOption = options.JsonSerializerOptions; + + // Collect option types for parsing command-line arguments. + var optionTypeByOptionName = new Dictionary(StringComparer.OrdinalIgnoreCase); + for (int i = 0; i < parameters.Length; i++) + { + var item = parameters[i]; + var option = item.GetCustomAttribute(); + + optionTypeByOptionName[(isStrict ? "--" : "") + options.NameConverter(item.Name!)] = item.ParameterType; + if (!string.IsNullOrWhiteSpace(option?.ShortName)) + { + optionTypeByOptionName[(isStrict ? "-" : "") + option!.ShortName!] = item.ParameterType; + } + } + + var (argumentDictionary, optionByIndex) = ParseArgument(args, argsOffset, optionTypeByOptionName, isStrict); + invokeArgs = new object[parameters.Length]; + + for (int i = 0; i < parameters.Length; i++) + { + var item = parameters[i]; + var itemName = options.NameConverter(item.Name!); + var option = item.GetCustomAttribute(); + if (!string.IsNullOrWhiteSpace(option?.ShortName) && char.IsDigit(option!.ShortName, 0)) throw new InvalidOperationException($"Option '{itemName}' has a short name, but the short name must start with A-Z or a-z."); + + var value = default(OptionParameter); + + // Indexed arguments (e.g. [Option(0)]) + if (option != null && option.Index != -1) + { + if (optionByIndex.Count <= option.Index) + { + if (!item.HasDefaultValue) + { + throw new InvalidOperationException($"Required argument {option.Index} was not found in specified arguments."); + } + } + else + { + value = optionByIndex[option.Index]; + } + } + + // Keyed options (e.g. -foo -bar ) + var longName = (isStrict) ? ("--" + itemName) : itemName; + var shortName = (isStrict) ? ("-" + option?.ShortName?.TrimStart('-')) : option?.ShortName?.TrimStart('-'); + + if (value.Value != null || argumentDictionary.TryGetValue(longName!, out value) || argumentDictionary.TryGetValue(shortName ?? "", out value)) + { + if (parameters[i].ParameterType == typeof(bool) && value.Value == null) + { + invokeArgs[i] = value.BooleanSwitch; + continue; + } + + if (value.Value != null) + { + if (parameters[i].ParameterType == typeof(string)) + { + // when string, invoke directly(avoid JSON escape) + invokeArgs[i] = value.Value; + continue; + } + else if (parameters[i].ParameterType.IsEnum) + { + try + { + invokeArgs[i] = Enum.Parse(parameters[i].ParameterType, value.Value, true); + continue; + } + catch + { + errorMessage = "Parameter \"" + itemName + "\"" + " fail on Enum parsing."; + return false; + } + } + else if (typeof(System.Collections.IEnumerable).IsAssignableFrom(parameters[i].ParameterType) && !typeof(System.Collections.IDictionary).IsAssignableFrom(parameters[i].ParameterType)) + { + var v = value.Value; + if (!(v.StartsWith("[") && v.EndsWith("]"))) + { + var elemType = UnwrapCollectionElementType(parameters[i].ParameterType); + if (elemType == typeof(string)) + { + if (!(v.StartsWith("\"") && v.EndsWith("\""))) + { + v = "[" + string.Join(",", v.Split(' ', ',').Select(x => "\"" + x + "\"")) + "]"; + } + else + { + v = "[" + v + "]"; + } + } + else + { + v = "[" + string.Join(",", v.Trim('\'', '\"').Split(' ', ',')) + "]"; + } + } + try + { + invokeArgs[i] = JsonSerializer.Deserialize(v, parameters[i].ParameterType, jsonOption); + continue; + } + catch + { + errorMessage = "Parameter \"" + itemName + "\"" + " fail on JSON deserialize, please check type or JSON escape or add double-quotation."; + return false; + } + } + else + { + var v = value.Value; + try + { + try + { + invokeArgs[i] = JsonSerializer.Deserialize(v, parameters[i].ParameterType, jsonOption); + continue; + } + catch (JsonException) { // retry with double quotations if (!(v.StartsWith("\"") && v.EndsWith("\""))) { v = $"\"{v}\""; - } - invokeArgs[i] = JsonSerializer.Deserialize(v, parameters[i].ParameterType, jsonOption); - continue; - } - } - catch - { - errorMessage = "Parameter \"" + itemName + "\"" + " fail on JSON deserialize, please check type or JSON escape or add double-quotation."; - return false; - } - } - } - } - - if (item.HasDefaultValue) - { - invokeArgs[i] = item.DefaultValue; - } - else if (item.ParameterType == typeof(bool)) - { - // bool without default value should be considered that it has implicit default value of false. - invokeArgs[i] = false; - } - else - { - var name = itemName; - if (option?.ShortName != null) - { - name = itemName + "(" + "-" + option.ShortName + ")"; - } - errorMessage = "Required parameter \"" + name + "\"" + " not found in argument."; - return false; - } - } - - errorMessage = null; - return true; - } - catch (Exception ex) - { - invokeArgs = default!; - errorMessage = ex.Message; - return false; - } - } - - static Type? UnwrapCollectionElementType(Type collectionType) - { - if (collectionType.IsArray) - { - return collectionType.GetElementType(); - } - - foreach (var i in collectionType.GetInterfaces()) - { - if (i.IsGenericType && (i.GetGenericTypeDefinition() == typeof(IEnumerable<>))) - { - return i.GetGenericArguments()[0]; - } - } - - return null; - } - - static (ReadOnlyDictionary OptionByKey, IReadOnlyList OptionByIndex) ParseArgument(string?[] args, int argsOffset, IReadOnlyDictionary optionTypeByName, bool isStrict) - { - var dict = new Dictionary(args.Length, StringComparer.OrdinalIgnoreCase); - var options = new List(); - for (int i = argsOffset; i < args.Length;) - { - var arg = args[i++]; - if (arg is null || !arg.StartsWith("-")) - { - options.Add(new OptionParameter() { Value = arg }); - continue; // not key - } - - var key = (isStrict) ? arg : arg.TrimStart('-'); - - if (optionTypeByName.TryGetValue(key, out var optionType)) - { - if (optionType == typeof(bool)) - { - var boolValue = true; - if (i < args.Length) - { - var isTrue = args[i]?.Equals("true", StringComparison.OrdinalIgnoreCase); - var isFalse = args[i]?.Equals("false", StringComparison.OrdinalIgnoreCase); - if (isTrue != null && isTrue.Value) - { - boolValue = true; - } - else if (isFalse != null && isFalse.Value) - { - boolValue = false; - } - } - - dict.Add(key, new OptionParameter { BooleanSwitch = boolValue }); - } - else - { - if (args.Length <= i) - { - throw new ArgumentException($@"Value for parameter ""{key}"" is not provided."); - } - - var value = args[i]; - dict.Add(key, new OptionParameter { Value = value }); - i++; - } - } - else - { - // not key - options.Add(new OptionParameter() { Value = arg }); - } - } - - return (new ReadOnlyDictionary(dict), options); - } - - struct OptionParameter - { - public string? Value; - public bool BooleanSwitch; - } - } + } + invokeArgs[i] = JsonSerializer.Deserialize(v, parameters[i].ParameterType, jsonOption); + continue; + } + } + catch + { + errorMessage = "Parameter \"" + itemName + "\"" + " fail on JSON deserialize, please check type or JSON escape or add double-quotation."; + return false; + } + } + } + } + + if (item.HasDefaultValue) + { + invokeArgs[i] = item.DefaultValue; + } + else if (item.ParameterType == typeof(bool)) + { + // bool without default value should be considered that it has implicit default value of false. + invokeArgs[i] = false; + } + else + { + var name = itemName; + if (option?.ShortName != null) + { + name = itemName + "(" + "-" + option.ShortName + ")"; + } + errorMessage = "Required parameter \"" + name + "\"" + " not found in argument."; + return false; + } + } + + errorMessage = null; + return true; + } + catch (Exception ex) + { + invokeArgs = default!; + errorMessage = ex.Message; + return false; + } + } + + static Type? UnwrapCollectionElementType(Type collectionType) + { + if (collectionType.IsArray) + { + return collectionType.GetElementType(); + } + + foreach (var i in collectionType.GetInterfaces()) + { + if (i.IsGenericType && (i.GetGenericTypeDefinition() == typeof(IEnumerable<>))) + { + return i.GetGenericArguments()[0]; + } + } + + return null; + } + + static (ReadOnlyDictionary OptionByKey, IReadOnlyList OptionByIndex) ParseArgument(string?[] args, int argsOffset, IReadOnlyDictionary optionTypeByName, bool isStrict) + { + var dict = new Dictionary(args.Length, StringComparer.OrdinalIgnoreCase); + var options = new List(); + for (int i = argsOffset; i < args.Length;) + { + var arg = args[i++]; + if (arg is null || !arg.StartsWith("-")) + { + options.Add(new OptionParameter() { Value = arg }); + continue; // not key + } + + var key = (isStrict) ? arg : arg.TrimStart('-'); + + if (optionTypeByName.TryGetValue(key, out var optionType)) + { + if (optionType == typeof(bool)) + { + var boolValue = true; + if (i < args.Length) + { + var isTrue = args[i]?.Equals("true", StringComparison.OrdinalIgnoreCase); + var isFalse = args[i]?.Equals("false", StringComparison.OrdinalIgnoreCase); + if (isTrue != null && isTrue.Value) + { + boolValue = true; + } + else if (isFalse != null && isFalse.Value) + { + boolValue = false; + } + } + + dict.Add(key, new OptionParameter { BooleanSwitch = boolValue }); + } + else + { + if (args.Length <= i) + { + throw new ArgumentException($@"Value for parameter ""{key}"" is not provided."); + } + + var value = args[i]; + dict.Add(key, new OptionParameter { Value = value }); + i++; + } + } + else + { + // not key + options.Add(new OptionParameter() { Value = arg }); + } + } + + return (new ReadOnlyDictionary(dict), options); + } + + struct OptionParameter + { + public string? Value; + public bool BooleanSwitch; + } + } } \ No newline at end of file diff --git a/src/ConsoleAppFramework5/Command.cs b/src/ConsoleAppFramework5/Command.cs index a159d1e..b772818 100644 --- a/src/ConsoleAppFramework5/Command.cs +++ b/src/ConsoleAppFramework5/Command.cs @@ -1,8 +1,15 @@ using Microsoft.CodeAnalysis; using System.Diagnostics.CodeAnalysis; +using System.Reflection.Metadata; +using System.Text; namespace ConsoleAppFramework; +public enum MethodKind +{ + Lambda, Method, FunctionPointer +} + public record class Command { public required bool IsAsync { get; init; } // Task or Task @@ -10,21 +17,47 @@ public record class Command public required bool IsRootCommand { get; init; } public required string CommandName { get; set; } public required CommandParameter[] Parameters { get; init; } + public required MethodKind MethodKind { get; init; } - public string BuildDelegateSignature() + public string BuildDelegateSignature(out string? delegateType) { + if (MethodKind == MethodKind.Lambda && Parameters.Any(x => x.HasDefaultValue)) + { + delegateType = BuildDelegateType("RunCommand"); + return "RunCommand"; + } + + delegateType = null; + if (MethodKind == MethodKind.FunctionPointer) return BuildFunctionPointerDelegateSignature(); + if (IsAsync) { if (IsVoid) { // Func<...,Task> + if (Parameters.Length == 0) + { + return $"Func"; + } + else + { + var parameters = string.Join(", ", Parameters.Select(x => x.Type.ToFullyQualifiedFormatDisplayString())); + return $"Func<{parameters}, Task>"; + } } else { // Func<...,Task> + if (Parameters.Length == 0) + { + return $"Func>"; + } + else + { + var parameters = string.Join(", ", Parameters.Select(x => x.Type.ToFullyQualifiedFormatDisplayString())); + return $"Func<{parameters}, Task>"; + } } - // TODO: not yet. - throw new NotImplementedException(); } else { @@ -56,6 +89,35 @@ public string BuildDelegateSignature() } } } + + public string BuildFunctionPointerDelegateSignature() + { + var retType = (IsAsync, IsVoid) switch + { + (true, true) => "Task", + (true, false) => "Task", + (false, true) => "void", + (false, false) => "int" + }; + + var parameters = string.Join(", ", Parameters.Select(x => x.Type.ToFullyQualifiedFormatDisplayString())); + var comma = Parameters.Length > 0 ? ", " : ""; + return $"delegate* managed<{parameters}{comma}{retType}>"; + } + + public string BuildDelegateType(string delegateName) + { + var retType = (IsAsync, IsVoid) switch + { + (true, true) => "Task", + (true, false) => "Task", + (false, true) => "void", + (false, false) => "int" + }; + + var parameters = string.Join(", ", Parameters.Select(x => x.ToString())); + return $"delegate {retType} {delegateName}({parameters});"; + } } public record class CommandParameter @@ -65,6 +127,9 @@ public record class CommandParameter public required bool HasDefaultValue { get; init; } public object? DefaultValue { get; init; } public required ITypeSymbol? CustomParserType { get; init; } + public required bool IsFromServices { get; init; } + public required bool IsCancellationToken { get; init; } + public bool IsParsable => !(IsFromServices || IsCancellationToken); public string BuildParseMethod(int argCount, string argumentName, WellKnownTypes wellKnownTypes) { @@ -134,7 +199,7 @@ public string BuildParseMethod(int argCount, string argumentName, WellKnownTypes } } - public string DefaultValueToString() + public string DefaultValueToString(bool castValue = true) { if (DefaultValue is bool b) { @@ -146,8 +211,26 @@ public string DefaultValueToString() } if (DefaultValue == null) { + if (!castValue) return "null"; return $"({Type.ToFullyQualifiedFormatDisplayString()})null"; } + + if (!castValue) return DefaultValue.ToString(); return $"({Type.ToFullyQualifiedFormatDisplayString()}){DefaultValue}"; } + + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append(Type.ToFullyQualifiedFormatDisplayString()); + sb.Append(" "); + sb.Append(Name); + if (HasDefaultValue) + { + sb.Append(" = "); + sb.Append(DefaultValueToString(castValue: false)); + } + + return sb.ToString(); + } } \ No newline at end of file diff --git a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs index e27c870..2c8b9c2 100644 --- a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs @@ -44,6 +44,7 @@ namespace ConsoleAppFramework; using System; using System.Threading.Tasks; +using System.Runtime.InteropServices; using System.Diagnostics.CodeAnalysis; [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] @@ -69,8 +70,15 @@ public OptionAttribute(params string[] aliases) } } +[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] +internal sealed class FromServicesAttribute : Attribute +{ +} + internal static partial class ConsoleApp { + public static IServiceProvider? ServiceProvider { get; set; } + public static void Run(string[] args) { } @@ -85,11 +93,6 @@ static void ThrowArgumentParseFailed(string argumentName, string value) throw new ArgumentException($"Argument '{argumentName}' parse failed. value: {value}"); } - static void ThrowInvalidArgumentName(string name) - { - throw new ArgumentException($"Required argument '{name}' does not matched."); - } - static void ThrowRequiredArgumentNotParsed(string name) { throw new ArgumentException($"Require argument '{name}' does not parsed."); @@ -107,6 +110,48 @@ static void ThrowRequiredArgumentNotParsed(string name) } } } + + sealed class PosixSignalHandler : IDisposable + { + public CancellationToken Token => cancellationTokenSource.Token; + + CancellationTokenSource cancellationTokenSource; + + PosixSignalRegistration? sigInt; + PosixSignalRegistration? sigQuit; + PosixSignalRegistration? sigTerm; + + PosixSignalHandler() + { + cancellationTokenSource = new CancellationTokenSource(); + } + + public static PosixSignalHandler Register() + { + var handler = new PosixSignalHandler(); + + 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(); + } + + public void Dispose() + { + sigInt?.Dispose(); + sigQuit?.Dispose(); + sigTerm?.Dispose(); + } + } } """); } @@ -118,7 +163,7 @@ static void EmitConsoleAppRun(SourceProductionContext sourceProductionContext, ( var wellKnownTypes = new WellKnownTypes(model.Compilation); - var parser = new Parser(sourceProductionContext, node, model); + var parser = new Parser(sourceProductionContext, node, model, wellKnownTypes); var command = parser.ParseAndValidate(); if (command == null) { @@ -126,7 +171,9 @@ static void EmitConsoleAppRun(SourceProductionContext sourceProductionContext, ( } var emitter = new Emitter(sourceProductionContext, command, wellKnownTypes); - var code = emitter.Emit(); + + var isRunAsync = ((node.Expression as MemberAccessExpressionSyntax)?.Name.Identifier.Text == "RunAsync"); + var code = emitter.EmitRun(isRunAsync); sourceProductionContext.AddSource("ConsoleApp.Run.cs", $$""" namespace ConsoleAppFramework; diff --git a/src/ConsoleAppFramework5/Emitter.cs b/src/ConsoleAppFramework5/Emitter.cs index cbca195..c7e8543 100644 --- a/src/ConsoleAppFramework5/Emitter.cs +++ b/src/ConsoleAppFramework5/Emitter.cs @@ -5,29 +5,46 @@ namespace ConsoleAppFramework; internal class Emitter(SourceProductionContext context, Command command, WellKnownTypes wellKnownTypes) { - public string Emit() + public string EmitRun(bool isRunAsync) { - // prepare argument -> - // parse argument -> - // validate parsed -> - // execute + var hasCancellationToken = command.Parameters.Any(x => x.IsCancellationToken); + // prepare argument variables -> var prepareArgument = new StringBuilder(); + if (hasCancellationToken) + { + prepareArgument.AppendLine(" using var posixSignalHandler = PosixSignalHandler.Register();"); + } for (var i = 0; i < command.Parameters.Length; i++) { var parameter = command.Parameters[i]; - var defaultValue = parameter.HasDefaultValue ? parameter.DefaultValueToString() : $"default({parameter.Type.ToFullyQualifiedFormatDisplayString()})"; - prepareArgument.AppendLine($" var arg{i} = {defaultValue};"); - if (!parameter.HasDefaultValue) + if (parameter.IsParsable) + { + var defaultValue = parameter.HasDefaultValue ? parameter.DefaultValueToString() : $"default({parameter.Type.ToFullyQualifiedFormatDisplayString()})"; + prepareArgument.AppendLine($" var arg{i} = {defaultValue};"); + if (!parameter.HasDefaultValue) + { + prepareArgument.AppendLine($" var arg{i}Parsed = false;"); + } + } + else if (parameter.IsCancellationToken) + { + prepareArgument.AppendLine($" var arg{i} = posixSignalHandler.Token;"); + } + else if (parameter.IsFromServices) { - prepareArgument.AppendLine($" var arg{i}Parsed = false;"); + var type = parameter.Type.ToFullyQualifiedFormatDisplayString(); + prepareArgument.AppendLine($" var arg{i} = ({type})ServiceProvider!.GetService(typeof({type}))!;"); } } + // parse argument(fast, switch directly) -> var fastParseCase = new StringBuilder(); for (int i = 0; i < command.Parameters.Length; i++) { var parameter = command.Parameters[i]; + if (!parameter.IsParsable) continue; + fastParseCase.AppendLine($" case \"--{parameter.Name}\":"); fastParseCase.AppendLine($" {parameter.BuildParseMethod(i, parameter.Name, wellKnownTypes)}"); if (!parameter.HasDefaultValue) @@ -37,10 +54,13 @@ public string Emit() fastParseCase.AppendLine(" break;"); } + // parse argument(slow, if ignorecase) -> var slowIgnoreCaseParse = new StringBuilder(); for (int i = 0; i < command.Parameters.Length; i++) { var parameter = command.Parameters[i]; + if (!parameter.IsParsable) continue; + slowIgnoreCaseParse.AppendLine($" if (string.Equals(name, \"--{parameter.Name}\", StringComparison.OrdinalIgnoreCase))"); slowIgnoreCaseParse.AppendLine(" {"); slowIgnoreCaseParse.AppendLine($" {parameter.BuildParseMethod(i, parameter.Name, wellKnownTypes)}"); @@ -52,23 +72,80 @@ public string Emit() slowIgnoreCaseParse.AppendLine(" }"); } + // validate parsed -> var validateParsed = new StringBuilder(); for (int i = 0; i < command.Parameters.Length; i++) { var parameter = command.Parameters[i]; + if (!parameter.IsParsable) continue; + if (!parameter.HasDefaultValue) { validateParsed.AppendLine($" if (!arg{i}Parsed) ThrowRequiredArgumentNotParsed(\"{parameter.Name}\");"); } } + // invoke for sync/async, void/int var methodArguments = string.Join(", ", command.Parameters.Select((x, i) => $"arg{i}!")); + var invoke = new StringBuilder(); + if (hasCancellationToken) + { + invoke.AppendLine(" try"); + invoke.AppendLine(" {"); + invoke.Append(" "); + } + + if (command.IsAsync) + { + if (command.IsVoid) + { + if (isRunAsync) + { + invoke.AppendLine($" await command({methodArguments});"); + } + else + { + invoke.AppendLine($" command({methodArguments}).GetAwaiter().GetResult();"); + } + } + else + { + if (isRunAsync) + { + invoke.AppendLine($" Environment.ExitCode = await command({methodArguments});"); + } + else + { + invoke.AppendLine($" Environment.ExitCode = command({methodArguments}).GetAwaiter().GetResult();"); + } + } + } + else + { + if (command.IsVoid) + { + invoke.AppendLine($" command({methodArguments});"); + } + else + { + invoke.AppendLine($" Environment.ExitCode = command({methodArguments});"); + } + } + + if (hasCancellationToken) + { + invoke.AppendLine(" }"); + invoke.AppendLine(" catch (OperationCanceledException ex) when (ex.CancellationToken == posixSignalHandler.Token) { }"); + } + + var returnType = isRunAsync ? "async Task" : "void"; + var methodName = isRunAsync ? "RunAsync" : "Run"; + var unsafeCode = (command.MethodKind == MethodKind.FunctionPointer) ? "unsafe " : ""; + + var commandMethodType = command.BuildDelegateSignature(out var delegateType); - // TODO: Run or RunAsync - // TODO: void or int and handle it. - // isASync, need GetAwaiter().GetResult(); var code = $$""" - public static void Run(string[] args, {{command.BuildDelegateSignature()}} command) + public static {{unsafeCode}}{{returnType}} {{methodName}}(string[] args, {{commandMethodType}} command) { {{prepareArgument}} for (int i = 0; i < args.Length; i++) @@ -80,16 +157,24 @@ public static void Run(string[] args, {{command.BuildDelegateSignature()}} comma {{fastParseCase}} default: {{slowIgnoreCaseParse}} - ThrowInvalidArgumentName(name); break; } } {{validateParsed}} - command({{methodArguments}}); +{{invoke}} } """; + if (delegateType != null) + { + code += $$""" + + + internal {{delegateType}} +"""; + } + return code; } } diff --git a/src/ConsoleAppFramework5/Parser.cs b/src/ConsoleAppFramework5/Parser.cs index e6a572c..e60c127 100644 --- a/src/ConsoleAppFramework5/Parser.cs +++ b/src/ConsoleAppFramework5/Parser.cs @@ -1,10 +1,12 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Net.Sockets; +using System.Reflection.Metadata; namespace ConsoleAppFramework; -internal class Parser(SourceProductionContext context, InvocationExpressionSyntax node, SemanticModel model) +internal class Parser(SourceProductionContext context, InvocationExpressionSyntax node, SemanticModel model, WellKnownTypes wellKnownTypes) { public Command? ParseAndValidate() { @@ -14,132 +16,232 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta var lambda = args[1].Expression as ParenthesizedLambdaExpressionSyntax; if (lambda == null) { - // TODO: validation(ReportDiagnostic) + if (args[1].Expression.IsKind(SyntaxKind.AddressOfExpression)) + { + var operand = (args[1].Expression as PrefixUnaryExpressionSyntax)!.Operand; + + var methodSymbols = model.GetMemberGroup(operand); + if (methodSymbols.Length > 0 && methodSymbols[0] is IMethodSymbol methodSymbol) + { + return ParseFromMethodSymbol(methodSymbol, addressOf: true); + } + } + else + { + var methodSymbols = model.GetMemberGroup(args[1].Expression); + if (methodSymbols.Length > 0 && methodSymbols[0] is IMethodSymbol methodSymbol) + { + return ParseFromMethodSymbol(methodSymbol, addressOf: false); + } + } + return null; } - - if (!lambda.Modifiers.Any(x => x.IsKind(SyntaxKind.StaticKeyword))) + else { - // TODO: validation(need static) - return null; + return ParseFromLambda(lambda); } + } + + return null; + } + + Command? ParseFromLambda(ParenthesizedLambdaExpressionSyntax lambda) + { + if (!lambda.Modifiers.Any(x => x.IsKind(SyntaxKind.StaticKeyword))) + { + // TODO: validation(need static) + return null; + } - // TODO: check return type + // TODO: check return type - var isAsync = lambda.AsyncKeyword.IsKind(SyntaxKind.AsyncKeyword); + var isAsync = lambda.AsyncKeyword.IsKind(SyntaxKind.AsyncKeyword); - var isVoid = lambda.ReturnType == null; - if (!isVoid) + var isVoid = lambda.ReturnType == null; + if (!isVoid) + { + if (!isAsync) { - if (!isAsync) + var keyword = (lambda.ReturnType as PredefinedTypeSyntax)?.Keyword; + if (keyword != null && keyword.Value.IsKind(SyntaxKind.VoidKeyword)) { - var keyword = (lambda.ReturnType as PredefinedTypeSyntax)?.Keyword; - if (keyword != null && keyword.Value.IsKind(SyntaxKind.VoidKeyword)) - { - isVoid = true; - } - else if (keyword != null && keyword.Value.IsKind(SyntaxKind.IntKeyword)) - { - isVoid = false; // ok - } - else - { - // others, invalid. - // TODO: validation invalid. - } + isVoid = true; + } + else if (keyword != null && keyword.Value.IsKind(SyntaxKind.IntKeyword)) + { + isVoid = false; // ok } else { - var firstType = (lambda.ReturnType as GenericNameSyntax)?.TypeArgumentList.Arguments.FirstOrDefault(); - if (firstType == null) - { - isVoid = true; // strictly, should check ret-type is Task... - } - else if ((firstType as PredefinedTypeSyntax)?.Keyword.IsKind(SyntaxKind.IntKeyword) ?? false) - { - isVoid = false; - } - else - { - // TODO: validation invalid - } + // others, invalid. + // TODO: validation invalid. + } + } + else + { + var firstType = (lambda.ReturnType as GenericNameSyntax)?.TypeArgumentList.Arguments.FirstOrDefault(); + if (firstType == null) + { + isVoid = true; // strictly, should check ret-type is Task... + } + else if ((firstType as PredefinedTypeSyntax)?.Keyword.IsKind(SyntaxKind.IntKeyword) ?? false) + { + isVoid = false; + } + else + { + // TODO: validation invalid } } + } + + var parameters = lambda.ParameterList.Parameters + .Where(x => x.Type != null) + .Select(x => + { + var type = model.GetTypeInfo(x.Type!); - var parameters = lambda.ParameterList.Parameters - .Where(x => x.Type != null) - .Select(x => + var hasDefault = x.Default != null; + object? defaultValue = null; + if (x.Default?.Value is LiteralExpressionSyntax literal) { - var type = model.GetTypeInfo(x.Type!); + var token = literal.Token; + defaultValue = token.Value; + } - var hasDefault = x.Default != null; - object? defaultValue = null; - if (x.Default?.Value is LiteralExpressionSyntax literal) - { - var token = literal.Token; - defaultValue = token.Value; - } + // bool is always optional flag + if (type.Type?.SpecialType == SpecialType.System_Boolean) + { + hasDefault = true; + defaultValue = false; + } - // bool is always optional flag - if (type.Type?.SpecialType == SpecialType.System_Boolean) + var parserAttr = x.AttributeLists.SelectMany(x => x.Attributes) + .FirstOrDefault(x => + { + var name = x.Name; + if (x.Name is QualifiedNameSyntax qns) + { + name = qns.Right; + } + + var identifier = (name as GenericNameSyntax)?.Identifier; + return identifier?.ValueText is "Parser" or "ParserAttribute"; + }); + + var isFromServices = x.AttributeLists.SelectMany(x => x.Attributes) + .Any(x => + { + var name = x.Name; + if (x.Name is QualifiedNameSyntax qns) + { + name = qns.Right; + } + + var identifier = name.ToString(); + return identifier is "FromServices" or "FromServicesAttribute"; + }); + + ITypeSymbol? customParserType = null; + if (parserAttr != null) + { + var name = parserAttr.Name; + if (parserAttr.Name is QualifiedNameSyntax qns) { - hasDefault = true; - defaultValue = false; + name = qns.Right; } - - var commandAttr = x.AttributeLists.SelectMany(x => x.Attributes) - .FirstOrDefault(x => - { - var name = x.Name; - if (x.Name is QualifiedNameSyntax qns) - { - name = qns.Right; - } - - var identifier = (name as GenericNameSyntax)?.Identifier; - return identifier?.ValueText is "Parser" or "ParserAttribute"; - }); - - ITypeSymbol? customParserType = null; - if (commandAttr != null) + var parserType = (name as GenericNameSyntax)?.TypeArgumentList.Arguments[0]; + if (parserType != null) { - var name = commandAttr.Name; - if (commandAttr.Name is QualifiedNameSyntax qns) - { - name = qns.Right; - } - var parserType = (name as GenericNameSyntax)?.TypeArgumentList.Arguments[0]; - if (parserType != null) - { - customParserType = model.GetTypeInfo(parserType).Type; - } - // TODO: validation, Type is IParsable? + customParserType = model.GetTypeInfo(parserType).Type; } + // TODO: validation, Type is IParsable? + } - return new CommandParameter - { - Name = x.Identifier.Text, - Type = type.Type!, - HasDefaultValue = hasDefault, - DefaultValue = defaultValue, - CustomParserType = customParserType, - }; - }) - .Where(x => x.Type != null) - .ToArray(); - - var cmd = new Command - { - CommandName = "", - IsAsync = isAsync, - IsRootCommand = true, - IsVoid = isVoid, - Parameters = parameters - }; - - return cmd; + var isCancellationToken = SymbolEqualityComparer.Default.Equals(type.Type!, wellKnownTypes.CancellationToken); + + return new CommandParameter + { + Name = x.Identifier.Text, + Type = type.Type!, + HasDefaultValue = hasDefault, + DefaultValue = defaultValue, + CustomParserType = customParserType, + IsCancellationToken = isCancellationToken, + IsFromServices = isFromServices + }; + }) + .Where(x => x.Type != null) + .ToArray(); + + var cmd = new Command + { + CommandName = "", + IsAsync = isAsync, + IsRootCommand = true, + IsVoid = isVoid, + Parameters = parameters, + MethodKind = MethodKind.Lambda + }; + + return cmd; + } + + Command? ParseFromMethodSymbol(IMethodSymbol methodSymbol, bool addressOf) + { + // allow returnType = void, int, Task, Task + var isVoid = false; + var isAsync = false; + if (methodSymbol.ReturnType.SpecialType == SpecialType.System_Void) + { + isVoid = true; + } + else if (methodSymbol.ReturnType.SpecialType == SpecialType.System_Int32) + { + isVoid = false; } - return null; + // TODO: check for Task, Task + + var parameters = methodSymbol.Parameters + .Select(x => + { + var parserAttr = x.GetAttributes().FirstOrDefault(x => x.AttributeClass?.Name == "ParserAttribute"); + + if (parserAttr != null) + { + // TODO: get parser + } + + // TODO: check FromServcies Attribute + + + var isCancellationToken = SymbolEqualityComparer.Default.Equals(x.Type, wellKnownTypes.CancellationToken); + + return new CommandParameter + { + Name = x.Name, + Type = x.Type, + HasDefaultValue = x.HasExplicitDefaultValue, + DefaultValue = x.HasExplicitDefaultValue ? x.ExplicitDefaultValue : null, + CustomParserType = null, + IsCancellationToken = isCancellationToken, + IsFromServices = false + }; + }) + .ToArray(); + + var cmd = new Command + { + CommandName = "", + IsAsync = isAsync, + IsRootCommand = true, + IsVoid = isVoid, + Parameters = parameters, + MethodKind = addressOf ? MethodKind.FunctionPointer : MethodKind.Method + }; + + return cmd; } } diff --git a/src/ConsoleAppFramework5/WellKnownTypes.cs b/src/ConsoleAppFramework5/WellKnownTypes.cs index 9446c66..fa86a9a 100644 --- a/src/ConsoleAppFramework5/WellKnownTypes.cs +++ b/src/ConsoleAppFramework5/WellKnownTypes.cs @@ -16,6 +16,9 @@ public class WellKnownTypes(Compilation compilation) INamedTypeSymbol? parsable; public INamedTypeSymbol? IParsable => parsable ??= compilation.GetTypeByMetadataName("System.IParsable`1"); + INamedTypeSymbol? cancellationToken; + public INamedTypeSymbol? CancellationToken => cancellationToken ??= compilation.GetTypeByMetadataName("System.Threading.CancellationToken"); + public bool HasTryParse(ITypeSymbol type) { if (SymbolEqualityComparer.Default.Equals(type, DateTimeOffset) From 285b47159160a0fdb2b363d17103da4e1126cf8c Mon Sep 17 00:00:00 2001 From: neuecc Date: Mon, 13 May 2024 20:39:28 +0900 Subject: [PATCH 03/54] working working --- sandbox/CliFrameworkBenchmark/Benchmark.cs | 61 ++++---- .../Commands/ConsoleAppFrameworkCommand.cs | 16 +- sandbox/GeneratorSandbox/Program.cs | 148 ++++++++++++++++-- src/ConsoleAppFramework5/Command.cs | 5 +- .../ConsoleAppGenerator.cs | 5 + src/ConsoleAppFramework5/Emitter.cs | 12 +- src/ConsoleAppFramework5/Parser.cs | 62 ++++++-- src/ConsoleAppFramework5/RoslynExtensions.cs | 77 +++++++++ 8 files changed, 332 insertions(+), 54 deletions(-) diff --git a/sandbox/CliFrameworkBenchmark/Benchmark.cs b/sandbox/CliFrameworkBenchmark/Benchmark.cs index b04fb73..14cfe20 100644 --- a/sandbox/CliFrameworkBenchmark/Benchmark.cs +++ b/sandbox/CliFrameworkBenchmark/Benchmark.cs @@ -18,51 +18,50 @@ namespace Cocona.Benchmark.External; public class Benchmark { private static readonly string[] Arguments = { "--str", "hello world", "-i", "13", "-b" }; - private static readonly string[] Arguments2 = { "--str", "hello world", "--i", "13", "--b" }; - //[Benchmark(Description = "Cocona.Lite", Baseline = true)] - //public async Task ExecuteWithCoconaLite() => - // await Cocona.CoconaLiteApp.RunAsync(Arguments); + [Benchmark(Description = "Cocona.Lite", Baseline = true)] + public async Task ExecuteWithCoconaLite() => + await Cocona.CoconaLiteApp.RunAsync(Arguments); - //[Benchmark(Description = "Cocona")] - //public async ValueTask ExecuteWithCocona() => - // await Cocona.CoconaApp.RunAsync(Arguments); + [Benchmark(Description = "Cocona")] + public async ValueTask ExecuteWithCocona() => + await Cocona.CoconaApp.RunAsync(Arguments); - ////[Benchmark(Description = "ConsoleAppFramework")] - ////public async ValueTask ExecuteWithConsoleAppFramework() => - //// await Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(Arguments); + //[Benchmark(Description = "ConsoleAppFramework")] + //public async ValueTask ExecuteWithConsoleAppFramework() => + // await Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(Arguments); - //[Benchmark(Description = "CliFx")] - //public async ValueTask ExecuteWithCliFx() => - // await new CliApplicationBuilder().AddCommand(typeof(CliFxCommand)).Build().RunAsync(Arguments); + [Benchmark(Description = "CliFx")] + public async ValueTask ExecuteWithCliFx() => + await new CliApplicationBuilder().AddCommand(typeof(CliFxCommand)).Build().RunAsync(Arguments); - //[Benchmark(Description = "System.CommandLine")] - //public async Task ExecuteWithSystemCommandLine() => - // await new SystemCommandLineCommand().ExecuteAsync(Arguments); + [Benchmark(Description = "System.CommandLine")] + public async Task ExecuteWithSystemCommandLine() => + await new SystemCommandLineCommand().ExecuteAsync(Arguments); - //[Benchmark(Description = "McMaster.Extensions.CommandLineUtils")] - //public int ExecuteWithMcMaster() => - // McMaster.Extensions.CommandLineUtils.CommandLineApplication.Execute(Arguments); + [Benchmark(Description = "McMaster.Extensions.CommandLineUtils")] + public int ExecuteWithMcMaster() => + McMaster.Extensions.CommandLineUtils.CommandLineApplication.Execute(Arguments); - //[Benchmark(Description = "CommandLineParser")] - //public void ExecuteWithCommandLineParser() => - // new Parser() - // .ParseArguments(Arguments, typeof(CommandLineParserCommand)) - // .WithParsed(c => c.Execute()); + [Benchmark(Description = "CommandLineParser")] + public void ExecuteWithCommandLineParser() => + new Parser() + .ParseArguments(Arguments, typeof(CommandLineParserCommand)) + .WithParsed(c => c.Execute()); - //[Benchmark(Description = "PowerArgs")] - //public void ExecuteWithPowerArgs() => - // PowerArgs.Args.InvokeMain(Arguments); + [Benchmark(Description = "PowerArgs")] + public void ExecuteWithPowerArgs() => + PowerArgs.Args.InvokeMain(Arguments); - //[Benchmark(Description = "Clipr")] - //public void ExecuteWithClipr() => - // clipr.CliParser.Parse(Arguments).Execute(); + [Benchmark(Description = "Clipr")] + public void ExecuteWithClipr() => + clipr.CliParser.Parse(Arguments).Execute(); [Benchmark(Description = "ConsoleAppFramework v5")] public void ExecuteConsoleAppFramework5() { - ConsoleApp.Run(Arguments2, static (string str, int i, bool b) => { }); + ConsoleApp.Run(Arguments, ConsoleAppFrameworkCommand.Execute); } //[Benchmark(Description = "ConsoleAppFramework v5(FP)")] diff --git a/sandbox/CliFrameworkBenchmark/Commands/ConsoleAppFrameworkCommand.cs b/sandbox/CliFrameworkBenchmark/Commands/ConsoleAppFrameworkCommand.cs index 184bb68..592ec67 100644 --- a/sandbox/CliFrameworkBenchmark/Commands/ConsoleAppFrameworkCommand.cs +++ b/sandbox/CliFrameworkBenchmark/Commands/ConsoleAppFrameworkCommand.cs @@ -13,4 +13,18 @@ // bool boolOption) // { // } -//} \ No newline at end of file +//} + +public class ConsoleAppFrameworkCommand +{ + /// + /// + /// + /// -s + /// -i + /// -b + public static void Execute(string? str, int intOption, bool boolOption) + { + + } +} \ No newline at end of file diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index 7fd91fd..0572864 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -11,7 +11,7 @@ using Microsoft.Extensions.DependencyInjection; using Takoyaki; -args = ["--x", "10"]; // test. +args = ["--x", "10", "--y", "20"]; // test. // var s = "foo"; @@ -29,6 +29,8 @@ // --x +ConsoleApp.Run(args, Command.Execute); + // description // @@ -53,13 +55,13 @@ // sp.GetService(); -await ConsoleApp.RunAsync(args, static async (int x, [FromServices] MyClass mc, CancellationToken cancellationToken) => -{ - Console.WriteLine((x, mc)); - await Task.Yield(); - await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken); - Console.WriteLine("end"); -}); +//await ConsoleApp.RunAsync(args, static async (int x, [FromServices] MyClass mc, CancellationToken cancellationToken) => +//{ +// Console.WriteLine((x, mc)); +// await Task.Yield(); +// await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken); +// Console.WriteLine("end"); +//}); //ConsoleApp.Run(args, &Methods.Method); @@ -72,14 +74,20 @@ static void Foo(RunRun rrrr) internal delegate void RunRun(int x, int y = 100); -public static class Methods +public static class Command { - public static void Method(int x, int y = 12345) + /// + /// Fuga Fuga + /// + /// -h, left x + /// -w|--woorong, world + public static void Execute(int hello, int world = 12345, CancellationToken cancellationToken = default) { } } + public class MyClass { @@ -150,7 +158,127 @@ public static bool TryParse([NotNullWhen(true)] string? s, [MaybeNullWhen(false) namespace ConsoleAppFramework { + partial class ConsoleApp + { + public static async void Run2(string[] args, Action command) + { + using var posixSignalHandler = PosixSignalHandler.Register(); + var arg0 = default(int); + var arg0Parsed = false; + var arg1 = (int)12345; + var arg2 = posixSignalHandler.Token; + + for (int i = 0; i < args.Length; i++) + { + var name = args[i]; + + switch (name) + { + case "--hello": + case "-h": + if (!int.TryParse(args[++i], out arg0)) ThrowArgumentParseFailed("hello", args[i]); + arg0Parsed = true; + break; + case "--world": + case "-w": + case "--woorong": + if (!int.TryParse(args[++i], out arg1)) ThrowArgumentParseFailed("world", args[i]); + break; + + default: + if (string.Equals(name, "--hello", StringComparison.OrdinalIgnoreCase) + || string.Equals(name, "-h", StringComparison.OrdinalIgnoreCase)) + { + if (!int.TryParse(args[++i], out arg0)) ThrowArgumentParseFailed("hello", args[i]); + arg0Parsed = true; + break; + } + if (string.Equals(name, "--world", StringComparison.OrdinalIgnoreCase) + || string.Equals(name, "-w", StringComparison.OrdinalIgnoreCase) + || string.Equals(name, "--woorong", StringComparison.OrdinalIgnoreCase)) + { + if (!int.TryParse(args[++i], out arg1)) ThrowArgumentParseFailed("world", args[i]); + break; + } + + ThrowArgumentNameNotFound(name); + break; + } + } + + if (!arg0Parsed) ThrowRequiredArgumentNotParsed("hello"); + + + //posixSignalHandler. + + try + { + var commandRun = Task.Run(() => command(arg0!, arg1!, arg2!)); + var t = await Task.WhenAny(commandRun, new PosixSignalHandler2().TimeoutAfterCanceled); + if (t != commandRun) // success + { + // Timeout. + + } + } + catch (OperationCanceledException ex) when (ex.CancellationToken == posixSignalHandler.Token) { } + + } + } + + sealed class PosixSignalHandler2 : IDisposable + { + public CancellationToken Token => cancellationTokenSource.Token; + public Task TimeoutAfterCanceled => timeoutTask.Task; + public TimeSpan Timeout; + CancellationTokenSource cancellationTokenSource; + CancellationTokenSource? timeoutCancellationTokenSource; + TaskCompletionSource timeoutTask; + PosixSignalRegistration? sigInt; + PosixSignalRegistration? sigQuit; + PosixSignalRegistration? sigTerm; + + public PosixSignalHandler2() + { + cancellationTokenSource = new CancellationTokenSource(); + timeoutTask = new(); + } + + public static PosixSignalHandler2 Register() + { + var handler = new PosixSignalHandler2(); + + 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; + } + + async void HandlePosixSignal(PosixSignalContext context) + { + context.Cancel = true; + cancellationTokenSource?.Cancel(); + timeoutCancellationTokenSource = new CancellationTokenSource(); + try + { + await Task.Delay(Timeout, timeoutCancellationTokenSource.Token); + } + catch (OperationCanceledException) { } + timeoutTask.TrySetResult(); + } + + public void Dispose() + { + sigInt?.Dispose(); + sigQuit?.Dispose(); + sigTerm?.Dispose(); + timeoutCancellationTokenSource?.Cancel(); + } + } } diff --git a/src/ConsoleAppFramework5/Command.cs b/src/ConsoleAppFramework5/Command.cs index b772818..a64ce32 100644 --- a/src/ConsoleAppFramework5/Command.cs +++ b/src/ConsoleAppFramework5/Command.cs @@ -15,8 +15,9 @@ public record class Command public required bool IsAsync { get; init; } // Task or Task public required bool IsVoid { get; init; } // void or int public required bool IsRootCommand { get; init; } - public required string CommandName { get; set; } + public required string CommandName { get; init; } public required CommandParameter[] Parameters { get; init; } + public required string Description { get; init; } public required MethodKind MethodKind { get; init; } public string BuildDelegateSignature(out string? delegateType) @@ -130,6 +131,8 @@ public record class CommandParameter public required bool IsFromServices { get; init; } public required bool IsCancellationToken { get; init; } public bool IsParsable => !(IsFromServices || IsCancellationToken); + public required string[] Aliases { get; init; } + public required string Description { get; init; } public string BuildParseMethod(int argCount, string argumentName, WellKnownTypes wellKnownTypes) { diff --git a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs index 2c8b9c2..ef95182 100644 --- a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs @@ -98,6 +98,11 @@ static void ThrowRequiredArgumentNotParsed(string name) throw new ArgumentException($"Require argument '{name}' does not parsed."); } + static void ThrowArgumentNameNotFound(string argumentName) + { + throw new ArgumentException($"Argument '{argumentName}' does not found in command prameters."); + } + static System.Collections.Generic.IEnumerable<(int start, int end)> Split(string str) { var start = 0; diff --git a/src/ConsoleAppFramework5/Emitter.cs b/src/ConsoleAppFramework5/Emitter.cs index c7e8543..7ea7a27 100644 --- a/src/ConsoleAppFramework5/Emitter.cs +++ b/src/ConsoleAppFramework5/Emitter.cs @@ -46,6 +46,10 @@ public string EmitRun(bool isRunAsync) if (!parameter.IsParsable) continue; fastParseCase.AppendLine($" case \"--{parameter.Name}\":"); + foreach (var alias in parameter.Aliases) + { + fastParseCase.AppendLine($" case \"{alias}\":"); + } fastParseCase.AppendLine($" {parameter.BuildParseMethod(i, parameter.Name, wellKnownTypes)}"); if (!parameter.HasDefaultValue) { @@ -61,7 +65,12 @@ public string EmitRun(bool isRunAsync) var parameter = command.Parameters[i]; if (!parameter.IsParsable) continue; - slowIgnoreCaseParse.AppendLine($" if (string.Equals(name, \"--{parameter.Name}\", StringComparison.OrdinalIgnoreCase))"); + slowIgnoreCaseParse.AppendLine($" if (string.Equals(name, \"--{parameter.Name}\", StringComparison.OrdinalIgnoreCase){(parameter.Aliases.Length == 0 ? ")" : "")}"); + for (int j = 0; j < parameter.Aliases.Length; j++) + { + var alias = parameter.Aliases[j]; + slowIgnoreCaseParse.AppendLine($" || string.Equals(name, \"{alias}\", StringComparison.OrdinalIgnoreCase){(parameter.Aliases.Length == j + 1 ? ")" : "")}"); + } slowIgnoreCaseParse.AppendLine(" {"); slowIgnoreCaseParse.AppendLine($" {parameter.BuildParseMethod(i, parameter.Name, wellKnownTypes)}"); if (!parameter.HasDefaultValue) @@ -157,6 +166,7 @@ public string EmitRun(bool isRunAsync) {{fastParseCase}} default: {{slowIgnoreCaseParse}} + ThrowArgumentNameNotFound(name); break; } } diff --git a/src/ConsoleAppFramework5/Parser.cs b/src/ConsoleAppFramework5/Parser.cs index e60c127..0c81606 100644 --- a/src/ConsoleAppFramework5/Parser.cs +++ b/src/ConsoleAppFramework5/Parser.cs @@ -48,11 +48,12 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta Command? ParseFromLambda(ParenthesizedLambdaExpressionSyntax lambda) { - if (!lambda.Modifiers.Any(x => x.IsKind(SyntaxKind.StaticKeyword))) - { - // TODO: validation(need static) - return null; - } + // allow not static... + //if (!lambda.Modifiers.Any(x => x.IsKind(SyntaxKind.StaticKeyword))) + //{ + // // TODO: validation(need static) + // return null; + //} // TODO: check return type @@ -169,7 +170,9 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta DefaultValue = defaultValue, CustomParserType = customParserType, IsCancellationToken = isCancellationToken, - IsFromServices = isFromServices + IsFromServices = isFromServices, + Aliases = [], + Description = "" }; }) .Where(x => x.Type != null) @@ -182,7 +185,8 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta IsRootCommand = true, IsVoid = isVoid, Parameters = parameters, - MethodKind = MethodKind.Lambda + MethodKind = MethodKind.Lambda, + Description = "" }; return cmd; @@ -190,6 +194,15 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta Command? ParseFromMethodSymbol(IMethodSymbol methodSymbol, bool addressOf) { + var docComment = methodSymbol.DeclaringSyntaxReferences[0].GetSyntax().GetDocumentationCommentTriviaSyntax(); + var summary = ""; + Dictionary? parameterDescriptions = null; + if (docComment != null) + { + summary = docComment.GetSummary(); + parameterDescriptions = docComment.GetParams().ToDictionary(x => x.Name, x => x.Description); + } + // allow returnType = void, int, Task, Task var isVoid = false; var isAsync = false; @@ -216,9 +229,15 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta // TODO: check FromServcies Attribute - var isCancellationToken = SymbolEqualityComparer.Default.Equals(x.Type, wellKnownTypes.CancellationToken); + string description = ""; + string[] aliases = []; + if (parameterDescriptions != null && parameterDescriptions.TryGetValue(x.Name, out var desc)) + { + ParseParameterDescription(desc, out aliases, out description); + } + return new CommandParameter { Name = x.Name, @@ -227,7 +246,9 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta DefaultValue = x.HasExplicitDefaultValue ? x.ExplicitDefaultValue : null, CustomParserType = null, IsCancellationToken = isCancellationToken, - IsFromServices = false + IsFromServices = false, + Aliases = aliases, + Description = description }; }) .ToArray(); @@ -239,9 +260,30 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta IsRootCommand = true, IsVoid = isVoid, Parameters = parameters, - MethodKind = addressOf ? MethodKind.FunctionPointer : MethodKind.Method + MethodKind = addressOf ? MethodKind.FunctionPointer : MethodKind.Method, + Description = summary }; return cmd; } + + void ParseParameterDescription(string originalDescription, out string[] aliases, out string description) + { + // Example: + // -h|--help, This is a help. + + var splitOne = originalDescription.Split(','); + + // has alias + if (splitOne[0].TrimStart().StartsWith("-")) + { + aliases = splitOne[0].Split(['|'], StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToArray(); + description = string.Join("", splitOne.Skip(1)).Trim(); + } + else + { + aliases = []; + description = originalDescription; + } + } } diff --git a/src/ConsoleAppFramework5/RoslynExtensions.cs b/src/ConsoleAppFramework5/RoslynExtensions.cs index 79a6b8d..ec56e84 100644 --- a/src/ConsoleAppFramework5/RoslynExtensions.cs +++ b/src/ConsoleAppFramework5/RoslynExtensions.cs @@ -1,4 +1,6 @@ using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.CSharp; namespace ConsoleAppFramework; @@ -15,4 +17,79 @@ public static bool EqualsUnconstructedGenericType(this INamedTypeSymbol left, IN var r = right.IsGenericType ? right.ConstructUnboundGenericType() : right; return SymbolEqualityComparer.Default.Equals(l, r); } + + public static DocumentationCommentTriviaSyntax? GetDocumentationCommentTriviaSyntax(this SyntaxNode node) + { + // Hack note: + // ISymbol.GetDocumentationCommtentXml requirestrue. + // However, getting the DocumentationCommentTrivia of a SyntaxNode also requires the same condition. + // It can only be obtained when DocumentationMode is Parse or Diagnostic, but whenfalse, + // it becomes None, and the necessary Trivia cannot be obtained. + // Therefore, we will attempt to reparse and retrieve it. + + // About DocumentationMode and Trivia: https://github.com/dotnet/roslyn/issues/58210 + if (node.SyntaxTree.Options.DocumentationMode == DocumentationMode.None) + { + var withDocumentationComment = node.SyntaxTree.Options.WithDocumentationMode(DocumentationMode.Parse); + var code = node.ToFullString(); + var newTree = CSharpSyntaxTree.ParseText(code, (CSharpParseOptions)withDocumentationComment); + node = newTree.GetRoot(); + } + + foreach (var leadingTrivia in node.GetLeadingTrivia()) + { + if (leadingTrivia.GetStructure() is DocumentationCommentTriviaSyntax structure) + { + return structure; + } + } + + return null; + } + + static IEnumerable GetXmlElements(this SyntaxList content, string elementName) + { + foreach (XmlNodeSyntax syntax in content) + { + if (syntax is XmlEmptyElementSyntax emptyElement) + { + if (string.Equals(elementName, emptyElement.Name.ToString(), StringComparison.Ordinal)) + { + yield return emptyElement; + } + + continue; + } + + if (syntax is XmlElementSyntax elementSyntax) + { + if (string.Equals(elementName, elementSyntax.StartTag?.Name?.ToString(), StringComparison.Ordinal)) + { + yield return elementSyntax; + } + + continue; + } + } + } + + public static string GetSummary(this DocumentationCommentTriviaSyntax docComment) + { + var summary = docComment.Content.GetXmlElements("summary").FirstOrDefault() as XmlElementSyntax; + if (summary == null) return ""; + + return summary.Content.ToString().Replace("///", "").Trim(); + } + + public static IEnumerable<(string Name, string Description)> GetParams(this DocumentationCommentTriviaSyntax docComment) + { + foreach (var item in docComment.Content.GetXmlElements("param").OfType()) + { + var name = item.StartTag.Attributes.OfType().FirstOrDefault()?.Identifier.Identifier.ValueText.Replace("///", "").Trim() ?? ""; + var desc = item.Content.ToString().Replace("///", "").Trim() ?? ""; + yield return (name, desc); + } + + yield break; + } } From feaab69e438f2a5174c6ae6e61e78048508a63b4 Mon Sep 17 00:00:00 2001 From: neuecc Date: Tue, 14 May 2024 07:28:31 +0900 Subject: [PATCH 04/54] w --- sandbox/GeneratorSandbox/Program.cs | 179 ++++-------------- .../ConsoleAppGenerator.cs | 51 +++-- src/ConsoleAppFramework5/Emitter.cs | 104 +++++----- src/ConsoleAppFramework5/Parser.cs | 2 + src/ConsoleAppFramework5/WellKnownTypes.cs | 7 + 5 files changed, 121 insertions(+), 222 deletions(-) diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index 0572864..6949582 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -11,7 +11,7 @@ using Microsoft.Extensions.DependencyInjection; using Takoyaki; -args = ["--x", "10", "--y", "20"]; // test. +args = ["--hello", "10", "--world", "20"]; // test. // var s = "foo"; @@ -28,9 +28,12 @@ // --x +unsafe +{ -ConsoleApp.Run(args, Command.Execute); + ConsoleApp.Run(args, &Command.Execute); +} // description // @@ -41,39 +44,12 @@ ConsoleApp.ServiceProvider = provider; -//var cts = new CancellationTokenSource(); - -//var iii = 0; -//while (true) -//{ -// Thread.Sleep(TimeSpan.FromSeconds(1)); -// Console.WriteLine(iii++ + ", " + cts.IsCancellationRequested); -//} - - -//delegate* managed a = &Method; - -// sp.GetService(); - -//await ConsoleApp.RunAsync(args, static async (int x, [FromServices] MyClass mc, CancellationToken cancellationToken) => -//{ -// Console.WriteLine((x, mc)); -// await Task.Yield(); -// await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken); -// Console.WriteLine("end"); -//}); - - -//ConsoleApp.Run(args, &Methods.Method); - -static void Foo(RunRun rrrr) +static void RunRun(int x, int y) { + Console.WriteLine("Hello World!" + x + y); } -internal delegate void RunRun(int x, int y = 100); - - public static class Command { /// @@ -83,7 +59,9 @@ public static class Command /// -w|--woorong, world public static void Execute(int hello, int world = 12345, CancellationToken cancellationToken = default) { - + Console.WriteLine("go"); + Thread.Sleep(TimeSpan.FromSeconds(10)); + Console.WriteLine("end"); } } @@ -158,127 +136,36 @@ public static bool TryParse([NotNullWhen(true)] string? s, [MaybeNullWhen(false) namespace ConsoleAppFramework { - partial class ConsoleApp - { - public static async void Run2(string[] args, Action command) - { - using var posixSignalHandler = PosixSignalHandler.Register(); - var arg0 = default(int); - var arg0Parsed = false; - var arg1 = (int)12345; - var arg2 = posixSignalHandler.Token; - - for (int i = 0; i < args.Length; i++) - { - var name = args[i]; - - switch (name) - { - case "--hello": - case "-h": - if (!int.TryParse(args[++i], out arg0)) ThrowArgumentParseFailed("hello", args[i]); - arg0Parsed = true; - break; - case "--world": - case "-w": - case "--woorong": - if (!int.TryParse(args[++i], out arg1)) ThrowArgumentParseFailed("world", args[i]); - break; - - default: - if (string.Equals(name, "--hello", StringComparison.OrdinalIgnoreCase) - || string.Equals(name, "-h", StringComparison.OrdinalIgnoreCase)) - { - if (!int.TryParse(args[++i], out arg0)) ThrowArgumentParseFailed("hello", args[i]); - arg0Parsed = true; - break; - } - if (string.Equals(name, "--world", StringComparison.OrdinalIgnoreCase) - || string.Equals(name, "-w", StringComparison.OrdinalIgnoreCase) - || string.Equals(name, "--woorong", StringComparison.OrdinalIgnoreCase)) - { - if (!int.TryParse(args[++i], out arg1)) ThrowArgumentParseFailed("world", args[i]); - break; - } - - ThrowArgumentNameNotFound(name); - break; - } - } - - if (!arg0Parsed) ThrowRequiredArgumentNotParsed("hello"); - - - //posixSignalHandler. - - try - { - var commandRun = Task.Run(() => command(arg0!, arg1!, arg2!)); - var t = await Task.WhenAny(commandRun, new PosixSignalHandler2().TimeoutAfterCanceled); - if (t != commandRun) // success - { - // Timeout. - - } - } - catch (OperationCanceledException ex) when (ex.CancellationToken == posixSignalHandler.Token) { } - - } - } + //partial class ConsoleApp + //{ - sealed class PosixSignalHandler2 : IDisposable - { - public CancellationToken Token => cancellationTokenSource.Token; - public Task TimeoutAfterCanceled => timeoutTask.Task; - public TimeSpan Timeout; - - CancellationTokenSource cancellationTokenSource; - CancellationTokenSource? timeoutCancellationTokenSource; - TaskCompletionSource timeoutTask; - PosixSignalRegistration? sigInt; - PosixSignalRegistration? sigQuit; - PosixSignalRegistration? sigTerm; - - public PosixSignalHandler2() - { - cancellationTokenSource = new CancellationTokenSource(); - timeoutTask = new(); - } + // public ConsoleAppBuilder CreateBuilder() + // { + // return new ConsoleAppBuilder(); + // } + //} - public static PosixSignalHandler2 Register() - { - var handler = new PosixSignalHandler2(); - Action handleSignal = handler.HandlePosixSignal; + //public class ConsoleAppBuilder + //{ + // public void Add() + // { + // } - handler.sigInt = PosixSignalRegistration.Create(PosixSignal.SIGINT, handleSignal); - handler.sigQuit = PosixSignalRegistration.Create(PosixSignal.SIGQUIT, handleSignal); - handler.sigTerm = PosixSignalRegistration.Create(PosixSignal.SIGTERM, handleSignal); - return handler; - } - async void HandlePosixSignal(PosixSignalContext context) - { - context.Cancel = true; - cancellationTokenSource?.Cancel(); - timeoutCancellationTokenSource = new CancellationTokenSource(); - try - { - await Task.Delay(Timeout, timeoutCancellationTokenSource.Token); - } - catch (OperationCanceledException) { } - timeoutTask.TrySetResult(); - } + // public void Run(string[] args) + // { + // if (args.Length == 0 || args[0].StartsWith('-')) + // { + // // invoke root command + // } + // } - public void Dispose() - { - sigInt?.Dispose(); - sigQuit?.Dispose(); - sigTerm?.Dispose(); - timeoutCancellationTokenSource?.Cancel(); - } - } + // public void RunAsync(string[] args) + // { + // } + //} } diff --git a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs index ef95182..cde13a6 100644 --- a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs @@ -57,19 +57,6 @@ internal interface IParser static abstract bool TryParse([NotNullWhen(true)] string? s, [MaybeNullWhen(false)] out T result); } -[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] -internal sealed class OptionAttribute : Attribute -{ - public string[] Aliases { get; } - public string? Name { get; set; } - public string? Description { get; set; } - - public OptionAttribute(params string[] aliases) - { - this.Aliases = aliases; - } -} - [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] internal sealed class FromServicesAttribute : Attribute { @@ -77,7 +64,9 @@ internal sealed class FromServicesAttribute : Attribute internal static partial class ConsoleApp { + public static Action LogError { get; set; } = msg => Console.WriteLine(msg); public static IServiceProvider? ServiceProvider { get; set; } + public static TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(5); public static void Run(string[] args) { @@ -119,21 +108,26 @@ static void ThrowArgumentNameNotFound(string argumentName) 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() + public PosixSignalHandler(TimeSpan timeout) { - cancellationTokenSource = new CancellationTokenSource(); + this.cancellationTokenSource = new CancellationTokenSource(); + this.timeoutCancellationTokenSource = new CancellationTokenSource(); + this.timeout = timeout; } - public static PosixSignalHandler Register() + public static PosixSignalHandler Register(TimeSpan timeout) { - var handler = new PosixSignalHandler(); + var handler = new PosixSignalHandler(timeout); Action handleSignal = handler.HandlePosixSignal; @@ -147,7 +141,8 @@ public static PosixSignalHandler Register() void HandlePosixSignal(PosixSignalContext context) { context.Cancel = true; - cancellationTokenSource?.Cancel(); + cancellationTokenSource.Cancel(); + timeoutCancellationTokenSource.CancelAfter(timeout); } public void Dispose() @@ -155,6 +150,7 @@ public void Dispose() sigInt?.Dispose(); sigQuit?.Dispose(); sigTerm?.Dispose(); + timeoutCancellationTokenSource.Dispose(); } } } @@ -175,12 +171,29 @@ static void EmitConsoleAppRun(SourceProductionContext sourceProductionContext, ( return; } - var emitter = new Emitter(sourceProductionContext, command, wellKnownTypes); + var emitter = new Emitter(command, wellKnownTypes); var isRunAsync = ((node.Expression as MemberAccessExpressionSyntax)?.Name.Identifier.Text == "RunAsync"); var code = emitter.EmitRun(isRunAsync); sourceProductionContext.AddSource("ConsoleApp.Run.cs", $$""" +// +#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. + namespace ConsoleAppFramework; using System; diff --git a/src/ConsoleAppFramework5/Emitter.cs b/src/ConsoleAppFramework5/Emitter.cs index 7ea7a27..91d1e7f 100644 --- a/src/ConsoleAppFramework5/Emitter.cs +++ b/src/ConsoleAppFramework5/Emitter.cs @@ -3,7 +3,7 @@ namespace ConsoleAppFramework; -internal class Emitter(SourceProductionContext context, Command command, WellKnownTypes wellKnownTypes) +internal class Emitter(Command command, WellKnownTypes wellKnownTypes) { public string EmitRun(bool isRunAsync) { @@ -13,7 +13,7 @@ public string EmitRun(bool isRunAsync) var prepareArgument = new StringBuilder(); if (hasCancellationToken) { - prepareArgument.AppendLine(" using var posixSignalHandler = PosixSignalHandler.Register();"); + prepareArgument.AppendLine(" using var posixSignalHandler = PosixSignalHandler.Register(Timeout);"); } for (var i = 0; i < command.Parameters.Length; i++) { @@ -45,17 +45,17 @@ public string EmitRun(bool isRunAsync) var parameter = command.Parameters[i]; if (!parameter.IsParsable) continue; - fastParseCase.AppendLine($" case \"--{parameter.Name}\":"); + fastParseCase.AppendLine($" case \"--{parameter.Name}\":"); foreach (var alias in parameter.Aliases) { - fastParseCase.AppendLine($" case \"{alias}\":"); + fastParseCase.AppendLine($" case \"{alias}\":"); } - fastParseCase.AppendLine($" {parameter.BuildParseMethod(i, parameter.Name, wellKnownTypes)}"); + fastParseCase.AppendLine($" {parameter.BuildParseMethod(i, parameter.Name, wellKnownTypes)}"); if (!parameter.HasDefaultValue) { - fastParseCase.AppendLine($" arg{i}Parsed = true;"); + fastParseCase.AppendLine($" arg{i}Parsed = true;"); } - fastParseCase.AppendLine(" break;"); + fastParseCase.AppendLine(" break;"); } // parse argument(slow, if ignorecase) -> @@ -65,20 +65,20 @@ public string EmitRun(bool isRunAsync) var parameter = command.Parameters[i]; if (!parameter.IsParsable) continue; - slowIgnoreCaseParse.AppendLine($" if (string.Equals(name, \"--{parameter.Name}\", StringComparison.OrdinalIgnoreCase){(parameter.Aliases.Length == 0 ? ")" : "")}"); + slowIgnoreCaseParse.AppendLine($" if (string.Equals(name, \"--{parameter.Name}\", StringComparison.OrdinalIgnoreCase){(parameter.Aliases.Length == 0 ? ")" : "")}"); for (int j = 0; j < parameter.Aliases.Length; j++) { var alias = parameter.Aliases[j]; - slowIgnoreCaseParse.AppendLine($" || string.Equals(name, \"{alias}\", StringComparison.OrdinalIgnoreCase){(parameter.Aliases.Length == j + 1 ? ")" : "")}"); + slowIgnoreCaseParse.AppendLine($" || string.Equals(name, \"{alias}\", StringComparison.OrdinalIgnoreCase){(parameter.Aliases.Length == j + 1 ? ")" : "")}"); } - slowIgnoreCaseParse.AppendLine(" {"); - slowIgnoreCaseParse.AppendLine($" {parameter.BuildParseMethod(i, parameter.Name, wellKnownTypes)}"); + slowIgnoreCaseParse.AppendLine(" {"); + slowIgnoreCaseParse.AppendLine($" {parameter.BuildParseMethod(i, parameter.Name, wellKnownTypes)}"); if (!parameter.HasDefaultValue) { - slowIgnoreCaseParse.AppendLine($" arg{i}Parsed = true;"); + slowIgnoreCaseParse.AppendLine($" arg{i}Parsed = true;"); } - slowIgnoreCaseParse.AppendLine($" break;"); - slowIgnoreCaseParse.AppendLine(" }"); + slowIgnoreCaseParse.AppendLine($" break;"); + slowIgnoreCaseParse.AppendLine(" }"); } // validate parsed -> @@ -90,61 +90,45 @@ public string EmitRun(bool isRunAsync) if (!parameter.HasDefaultValue) { - validateParsed.AppendLine($" if (!arg{i}Parsed) ThrowRequiredArgumentNotParsed(\"{parameter.Name}\");"); + validateParsed.AppendLine($" if (!arg{i}Parsed) ThrowRequiredArgumentNotParsed(\"{parameter.Name}\");"); } } // invoke for sync/async, void/int var methodArguments = string.Join(", ", command.Parameters.Select((x, i) => $"arg{i}!")); - var invoke = new StringBuilder(); + var invokeCommand = $"command({methodArguments})"; if (hasCancellationToken) { - invoke.AppendLine(" try"); - invoke.AppendLine(" {"); - invoke.Append(" "); + invokeCommand = $"Task.Run(() => {invokeCommand}).WaitAsync(posixSignalHandler.TimeoutToken)"; } - - if (command.IsAsync) + if (command.IsAsync || hasCancellationToken) { - if (command.IsVoid) + if (isRunAsync) { - if (isRunAsync) - { - invoke.AppendLine($" await command({methodArguments});"); - } - else - { - invoke.AppendLine($" command({methodArguments}).GetAwaiter().GetResult();"); - } + invokeCommand = $"await {invokeCommand}"; } else { - if (isRunAsync) - { - invoke.AppendLine($" Environment.ExitCode = await command({methodArguments});"); - } - else - { - invoke.AppendLine($" Environment.ExitCode = command({methodArguments}).GetAwaiter().GetResult();"); - } + invokeCommand = $"{invokeCommand}.GetAwaiter().GetResult()"; } } + + var invoke = new StringBuilder(); + if (command.IsVoid) + { + invoke.AppendLine($" {invokeCommand};"); + } else { - if (command.IsVoid) - { - invoke.AppendLine($" command({methodArguments});"); - } - else - { - invoke.AppendLine($" Environment.ExitCode = command({methodArguments});"); - } + invoke.AppendLine($" Environment.ExitCode = {invokeCommand};"); } - + invoke.AppendLine(" }"); // try close if (hasCancellationToken) { + invoke.AppendLine(" catch (OperationCanceledException ex) when (ex.CancellationToken == posixSignalHandler.Token || ex.CancellationToken == posixSignalHandler.TimeoutToken)"); + invoke.AppendLine(" {"); + invoke.AppendLine(" Environment.ExitCode = 130;"); invoke.AppendLine(" }"); - invoke.AppendLine(" catch (OperationCanceledException ex) when (ex.CancellationToken == posixSignalHandler.Token) { }"); } var returnType = isRunAsync ? "async Task" : "void"; @@ -157,22 +141,28 @@ public string EmitRun(bool isRunAsync) public static {{unsafeCode}}{{returnType}} {{methodName}}(string[] args, {{commandMethodType}} command) { {{prepareArgument}} - for (int i = 0; i < args.Length; i++) + try { - var name = args[i]; - - switch (name) + for (int i = 0; i < args.Length; i++) { + var name = args[i]; + + switch (name) + { {{fastParseCase}} - default: + default: {{slowIgnoreCaseParse}} - ThrowArgumentNameNotFound(name); - break; + ThrowArgumentNameNotFound(name); + break; + } } - } - {{validateParsed}} {{invoke}} + catch (Exception ex) + { + Environment.ExitCode = 1; + LogError(ex.ToString()); + } } """; diff --git a/src/ConsoleAppFramework5/Parser.cs b/src/ConsoleAppFramework5/Parser.cs index 0c81606..ef76e2e 100644 --- a/src/ConsoleAppFramework5/Parser.cs +++ b/src/ConsoleAppFramework5/Parser.cs @@ -216,6 +216,8 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta } // TODO: check for Task, Task + //methodSymbol.ReturnType.SpecialType == SpecialType.System_Threading_Tasks_Task_T + var task = methodSymbol.ReturnType.Name == "Task"; var parameters = methodSymbol.Parameters .Select(x => diff --git a/src/ConsoleAppFramework5/WellKnownTypes.cs b/src/ConsoleAppFramework5/WellKnownTypes.cs index fa86a9a..6523ded 100644 --- a/src/ConsoleAppFramework5/WellKnownTypes.cs +++ b/src/ConsoleAppFramework5/WellKnownTypes.cs @@ -19,8 +19,15 @@ public class WellKnownTypes(Compilation compilation) INamedTypeSymbol? cancellationToken; public INamedTypeSymbol? CancellationToken => cancellationToken ??= compilation.GetTypeByMetadataName("System.Threading.CancellationToken"); + INamedTypeSymbol? task; + public INamedTypeSymbol? Task => task ??= compilation.GetTypeByMetadataName("System.Threading.Tasks.Task"); + + INamedTypeSymbol? task_T; + public INamedTypeSymbol? Task_T => task_T ??= compilation.GetTypeByMetadataName("System.Threading.Tasks.Task`1"); + public bool HasTryParse(ITypeSymbol type) { + if (SymbolEqualityComparer.Default.Equals(type, DateTimeOffset) || SymbolEqualityComparer.Default.Equals(type, Guid) || SymbolEqualityComparer.Default.Equals(type, Version) From 28cf07fb65ec8a65f6af8fef7baa9b26450902e9 Mon Sep 17 00:00:00 2001 From: neuecc Date: Tue, 14 May 2024 18:49:53 +0900 Subject: [PATCH 05/54] Argument --- sandbox/GeneratorSandbox/Program.cs | 123 +++++++++++++++--- src/ConsoleAppFramework5/Command.cs | 22 ++-- .../ConsoleAppGenerator.cs | 12 +- src/ConsoleAppFramework5/Emitter.cs | 27 +++- src/ConsoleAppFramework5/Parser.cs | 115 ++++++++++------ src/ConsoleAppFramework5/WellKnownTypes.cs | 10 +- 6 files changed, 226 insertions(+), 83 deletions(-) diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index 6949582..c5e437b 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -11,7 +11,7 @@ using Microsoft.Extensions.DependencyInjection; using Takoyaki; -args = ["--hello", "10", "--world", "20"]; // test. +args = ["100", "--y", "20"]; // test. // var s = "foo"; @@ -31,7 +31,14 @@ unsafe { - ConsoleApp.Run(args, &Command.Execute); + //ConsoleApp.Run(args, ([Vector3Parser] Vector3 x) => + //{Quaternion// + //}); + ConsoleApp.Run(args, ([Argument] int x, int y) => + { + Console.WriteLine(x + y); + }); + } @@ -44,24 +51,30 @@ ConsoleApp.ServiceProvider = provider; -static void RunRun(int x, int y) +static async Task RunRun([Vector3Parser] Vector3 x, [FromServices] MyClass y) { Console.WriteLine("Hello World!" + x + y); + return 0; +} + + +static void Tests() + where T : ISpanParsable +{ + + } public static class Command { /// - /// Fuga Fuga + /// /// - /// -h, left x - /// -w|--woorong, world - public static void Execute(int hello, int world = 12345, CancellationToken cancellationToken = default) + /// -f|--cho|/tako|*nano|-ZOMBI + /// + public static void Execute(int foo, CancellationToken cancellationToken) { - Console.WriteLine("go"); - Thread.Sleep(TimeSpan.FromSeconds(10)); - Console.WriteLine("end"); } } @@ -84,6 +97,7 @@ public static class Hoge { public static void Nano(int x) { + } } } @@ -99,21 +113,26 @@ public static void Nano(int x) //} -public interface IParser -{ - static abstract bool TryParse([NotNullWhen(true)] string? s, [MaybeNullWhen(false)] out T result); -} +//public interface IArgumentParser +//{ +// static abstract bool TryParse(ReadOnlySpan s, out T result); +//} +[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] +internal sealed class ArgumentAttribute : Attribute +{ +} -public readonly struct Vector3Parser : IParser +[AttributeUsage(AttributeTargets.Parameter)] +public class Vector3ParserAttribute : Attribute, IArgumentParser { - public static bool TryParse([NotNullWhen(true)] string? s, [MaybeNullWhen(false)] out Vector3 result) + public static bool TryParse(ReadOnlySpan s, out Vector3 result) { Span ranges = stackalloc Range[3]; - var splitCount = s.AsSpan().Split(ranges, ','); + var splitCount = s.Split(ranges, ','); if (splitCount != 3) { result = default; @@ -123,7 +142,7 @@ public static bool TryParse([NotNullWhen(true)] string? s, [MaybeNullWhen(false) float x; float y; float z; - if (float.TryParse(s.AsSpan(ranges[0]), out x) && float.TryParse(s.AsSpan(ranges[1]), out y) && float.TryParse(s.AsSpan(ranges[2]), out z)) + if (float.TryParse(s[ranges[0]], out x) && float.TryParse(s[ranges[1]], out y) && float.TryParse(s[ranges[2]], out z)) { result = new Vector3(x, y, z); return true; @@ -167,5 +186,73 @@ namespace ConsoleAppFramework // { // } //} + + partial class ConsoleApp + + { + public static void Run2(string[] args, Action command) + { + var arg0 = default(int); + var arg0Parsed = false; + var arg1 = default(int); + var arg1Parsed = false; + + try + { + for (int i = 0; i < args.Length; i++) + { + // add this block. + if (i == 0) + { + if (!int.TryParse(args[i], out arg0)) ThrowArgumentParseFailed("x", args[i]); // no++ + arg0Parsed = true; + continue; + } + + + var name = args[i]; + + switch (name) + { + case "--x": + if (!int.TryParse(args[++i], out arg0)) ThrowArgumentParseFailed("x", args[i]); + arg0Parsed = true; + break; + case "--y": + if (!int.TryParse(args[++i], out arg1)) ThrowArgumentParseFailed("y", args[i]); + arg1Parsed = true; + break; + + default: + if (string.Equals(name, "--x", StringComparison.OrdinalIgnoreCase)) + { + if (!int.TryParse(args[++i], out arg0)) ThrowArgumentParseFailed("x", args[i]); + arg0Parsed = true; + break; + } + if (string.Equals(name, "--y", StringComparison.OrdinalIgnoreCase)) + { + if (!int.TryParse(args[++i], out arg1)) ThrowArgumentParseFailed("y", args[i]); + arg1Parsed = true; + break; + } + + ThrowArgumentNameNotFound(name); + break; + } + } + if (!arg0Parsed) ThrowRequiredArgumentNotParsed("x"); + if (!arg1Parsed) ThrowRequiredArgumentNotParsed("y"); + + command(arg0!, arg1!); + } + + catch (Exception ex) + { + Environment.ExitCode = 1; + LogError(ex.ToString()); + } + } + } } diff --git a/src/ConsoleAppFramework5/Command.cs b/src/ConsoleAppFramework5/Command.cs index a64ce32..67c99fb 100644 --- a/src/ConsoleAppFramework5/Command.cs +++ b/src/ConsoleAppFramework5/Command.cs @@ -131,14 +131,18 @@ public record class CommandParameter public required bool IsFromServices { get; init; } public required bool IsCancellationToken { get; init; } public bool IsParsable => !(IsFromServices || IsCancellationToken); + public required int ArgumentIndex { get; init; } // -1 is not Argument, other than marked as [Argument] + public bool IsArgument => ArgumentIndex != -1; public required string[] Aliases { get; init; } public required string Description { get; init; } - public string BuildParseMethod(int argCount, string argumentName, WellKnownTypes wellKnownTypes) + public string BuildParseMethod(int argCount, string argumentName, WellKnownTypes wellKnownTypes, bool increment) { + var index = increment ? "++i" : "i"; + if (CustomParserType != null) { - return $"if (!{CustomParserType.ToFullyQualifiedFormatDisplayString()}.TryParse(args[++i], out arg{argCount})) ThrowArgumentParseFailed(\"{argumentName}\", args[i]);"; + return $"if (!{CustomParserType.ToFullyQualifiedFormatDisplayString()}.TryParse(args[{index}], out arg{argCount})) ThrowArgumentParseFailed(\"{argumentName}\", args[i]);"; } var tryParseKnownPrimitive = false; @@ -147,7 +151,7 @@ public string BuildParseMethod(int argCount, string argumentName, WellKnownTypes switch (Type.SpecialType) { case SpecialType.System_String: - return $"arg{argCount} = args[++i];"; // no parse + return $"arg{argCount} = args[{index}];"; // no parse case SpecialType.System_Boolean: return $"arg{argCount} = true;"; // bool is true flag case SpecialType.System_Char: @@ -169,7 +173,7 @@ public string BuildParseMethod(int argCount, string argumentName, WellKnownTypes // Enum if (Type.TypeKind == TypeKind.Enum) { - return $"if (!Enum.TryParse<{Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>(args[++i], true, out arg{argCount})) ThrowArgumentParseFailed(\"{argumentName}\", args[i]);"; + return $"if (!Enum.TryParse<{Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>(args[{index}], true, out arg{argCount})) ThrowArgumentParseFailed(\"{argumentName}\", args[i]);"; } // System.DateTimeOffset, System.Guid, System.Version @@ -177,8 +181,8 @@ public string BuildParseMethod(int argCount, string argumentName, WellKnownTypes if (!tryParseKnownPrimitive) { - // IParsable (BigInteger, Complex, Half, Int128, etc...) - var parsable = wellKnownTypes.IParsable; + // ISpanParsable (BigInteger, Complex, Half, Int128, etc...) + var parsable = wellKnownTypes.ISpanParsable; if (parsable != null) // has parsable { tryParseIParsable = Type.AllInterfaces.Any(x => x.EqualsUnconstructedGenericType(parsable)); @@ -190,15 +194,15 @@ public string BuildParseMethod(int argCount, string argumentName, WellKnownTypes if (tryParseKnownPrimitive) { - return $"if (!{Type.ToFullyQualifiedFormatDisplayString()}.TryParse(args[++i], out arg{argCount})) ThrowArgumentParseFailed(\"{argumentName}\", args[i]);"; + return $"if (!{Type.ToFullyQualifiedFormatDisplayString()}.TryParse(args[{index}], out arg{argCount})) ThrowArgumentParseFailed(\"{argumentName}\", args[i]);"; } else if (tryParseIParsable) { - return $"if (!{Type.ToFullyQualifiedFormatDisplayString()}.TryParse(args[++i], null, out arg{argCount})) ThrowArgumentParseFailed(\"{argumentName}\", args[i]);"; + return $"if (!{Type.ToFullyQualifiedFormatDisplayString()}.TryParse(args[{index}], null, out arg{argCount})) ThrowArgumentParseFailed(\"{argumentName}\", args[i]);"; } else { - return $"try {{ arg{argCount} = System.Text.Json.JsonSerializer.Deserialize<{Type.ToFullyQualifiedFormatDisplayString()}>(args[++i]); }} catch {{ ThrowArgumentParseFailed(\"{argumentName}\", args[i]); }}"; + return $"try {{ arg{argCount} = System.Text.Json.JsonSerializer.Deserialize<{Type.ToFullyQualifiedFormatDisplayString()}>(args[{index}]); }} catch {{ ThrowArgumentParseFailed(\"{argumentName}\", args[i]); }}"; } } diff --git a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs index cde13a6..1a7319c 100644 --- a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs @@ -47,18 +47,18 @@ namespace ConsoleAppFramework; using System.Runtime.InteropServices; using System.Diagnostics.CodeAnalysis; -[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] -internal sealed class ParserAttribute : Attribute +internal interface IArgumentParser { + static abstract bool TryParse(ReadOnlySpan s, out T result); } -internal interface IParser +[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] +internal sealed class FromServicesAttribute : Attribute { - static abstract bool TryParse([NotNullWhen(true)] string? s, [MaybeNullWhen(false)] out T result); } [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] -internal sealed class FromServicesAttribute : Attribute +internal sealed class ArgumentAttribute : Attribute { } @@ -118,7 +118,7 @@ sealed class PosixSignalHandler : IDisposable PosixSignalRegistration? sigQuit; PosixSignalRegistration? sigTerm; - public PosixSignalHandler(TimeSpan timeout) + PosixSignalHandler(TimeSpan timeout) { this.cancellationTokenSource = new CancellationTokenSource(); this.timeoutCancellationTokenSource = new CancellationTokenSource(); diff --git a/src/ConsoleAppFramework5/Emitter.cs b/src/ConsoleAppFramework5/Emitter.cs index 91d1e7f..7985850 100644 --- a/src/ConsoleAppFramework5/Emitter.cs +++ b/src/ConsoleAppFramework5/Emitter.cs @@ -1,4 +1,5 @@ using Microsoft.CodeAnalysis; +using System.Reflection.Metadata; using System.Text; namespace ConsoleAppFramework; @@ -8,6 +9,7 @@ internal class Emitter(Command command, WellKnownTypes wellKnownTypes) public string EmitRun(bool isRunAsync) { var hasCancellationToken = command.Parameters.Any(x => x.IsCancellationToken); + var hasArgument = command.Parameters.Any(x => x.IsArgument); // prepare argument variables -> var prepareArgument = new StringBuilder(); @@ -38,19 +40,38 @@ public string EmitRun(bool isRunAsync) } } + // parse indexed argument([Argument] parameter) + var indexedArgument = new StringBuilder(); + for (int i = 0; i < command.Parameters.Length; i++) + { + var parameter = command.Parameters[i]; + if (!parameter.IsArgument) continue; + + indexedArgument.AppendLine($" if (i == {parameter.ArgumentIndex})"); + indexedArgument.AppendLine(" {"); + indexedArgument.AppendLine($" {parameter.BuildParseMethod(i, parameter.Name, wellKnownTypes, increment: false)}"); + if (!parameter.HasDefaultValue) + { + indexedArgument.AppendLine($" arg{i}Parsed = true;"); + } + indexedArgument.AppendLine(" continue;"); + indexedArgument.AppendLine(" }"); + } + // parse argument(fast, switch directly) -> var fastParseCase = new StringBuilder(); for (int i = 0; i < command.Parameters.Length; i++) { var parameter = command.Parameters[i]; if (!parameter.IsParsable) continue; + if (parameter.IsArgument) continue; fastParseCase.AppendLine($" case \"--{parameter.Name}\":"); foreach (var alias in parameter.Aliases) { fastParseCase.AppendLine($" case \"{alias}\":"); } - fastParseCase.AppendLine($" {parameter.BuildParseMethod(i, parameter.Name, wellKnownTypes)}"); + fastParseCase.AppendLine($" {parameter.BuildParseMethod(i, parameter.Name, wellKnownTypes, increment: true)}"); if (!parameter.HasDefaultValue) { fastParseCase.AppendLine($" arg{i}Parsed = true;"); @@ -64,6 +85,7 @@ public string EmitRun(bool isRunAsync) { var parameter = command.Parameters[i]; if (!parameter.IsParsable) continue; + if (parameter.IsArgument) continue; slowIgnoreCaseParse.AppendLine($" if (string.Equals(name, \"--{parameter.Name}\", StringComparison.OrdinalIgnoreCase){(parameter.Aliases.Length == 0 ? ")" : "")}"); for (int j = 0; j < parameter.Aliases.Length; j++) @@ -72,7 +94,7 @@ public string EmitRun(bool isRunAsync) slowIgnoreCaseParse.AppendLine($" || string.Equals(name, \"{alias}\", StringComparison.OrdinalIgnoreCase){(parameter.Aliases.Length == j + 1 ? ")" : "")}"); } slowIgnoreCaseParse.AppendLine(" {"); - slowIgnoreCaseParse.AppendLine($" {parameter.BuildParseMethod(i, parameter.Name, wellKnownTypes)}"); + slowIgnoreCaseParse.AppendLine($" {parameter.BuildParseMethod(i, parameter.Name, wellKnownTypes, increment: true)}"); if (!parameter.HasDefaultValue) { slowIgnoreCaseParse.AppendLine($" arg{i}Parsed = true;"); @@ -145,6 +167,7 @@ public string EmitRun(bool isRunAsync) { for (int i = 0; i < args.Length; i++) { +{{indexedArgument}} var name = args[i]; switch (name) diff --git a/src/ConsoleAppFramework5/Parser.cs b/src/ConsoleAppFramework5/Parser.cs index ef76e2e..dc93199 100644 --- a/src/ConsoleAppFramework5/Parser.cs +++ b/src/ConsoleAppFramework5/Parser.cs @@ -1,8 +1,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -using System.Net.Sockets; -using System.Reflection.Metadata; namespace ConsoleAppFramework; @@ -48,15 +46,6 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta Command? ParseFromLambda(ParenthesizedLambdaExpressionSyntax lambda) { - // allow not static... - //if (!lambda.Modifiers.Any(x => x.IsKind(SyntaxKind.StaticKeyword))) - //{ - // // TODO: validation(need static) - // return null; - //} - - // TODO: check return type - var isAsync = lambda.AsyncKeyword.IsKind(SyntaxKind.AsyncKeyword); var isVoid = lambda.ReturnType == null; @@ -97,6 +86,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta } } + var parsableIndex = 0; var parameters = lambda.ParameterList.Parameters .Where(x => x.Type != null) .Select(x => @@ -118,8 +108,20 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta defaultValue = false; } - var parserAttr = x.AttributeLists.SelectMany(x => x.Attributes) - .FirstOrDefault(x => + var customParserType = x.AttributeLists.SelectMany(x => x.Attributes) + .Select(x => + { + var attr = model.GetTypeInfo(x).Type; + if (attr != null && attr.AllInterfaces.Any(x => x.Name == "IArgumentParser")) + { + return attr; + } + return null; + }) + .FirstOrDefault(x => x != null); + + var isFromServices = x.AttributeLists.SelectMany(x => x.Attributes) + .Any(x => { var name = x.Name; if (x.Name is QualifiedNameSyntax qns) @@ -127,11 +129,11 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta name = qns.Right; } - var identifier = (name as GenericNameSyntax)?.Identifier; - return identifier?.ValueText is "Parser" or "ParserAttribute"; + var identifier = name.ToString(); + return identifier is "FromServices" or "FromServicesAttribute"; }); - var isFromServices = x.AttributeLists.SelectMany(x => x.Attributes) + var hasArgument = x.AttributeLists.SelectMany(x => x.Attributes) .Any(x => { var name = x.Name; @@ -141,27 +143,24 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta } var identifier = name.ToString(); - return identifier is "FromServices" or "FromServicesAttribute"; + return identifier is "Argument" or "ArgumentAttribute"; }); - ITypeSymbol? customParserType = null; - if (parserAttr != null) + var isCancellationToken = SymbolEqualityComparer.Default.Equals(type.Type!, wellKnownTypes.CancellationToken); + + var argumentIndex = -1; + if (!(isFromServices || isCancellationToken)) { - var name = parserAttr.Name; - if (parserAttr.Name is QualifiedNameSyntax qns) + if (hasArgument) { - name = qns.Right; + argumentIndex = parsableIndex++; } - var parserType = (name as GenericNameSyntax)?.TypeArgumentList.Arguments[0]; - if (parserType != null) + else { - customParserType = model.GetTypeInfo(parserType).Type; + parsableIndex++; } - // TODO: validation, Type is IParsable? } - var isCancellationToken = SymbolEqualityComparer.Default.Equals(type.Type!, wellKnownTypes.CancellationToken); - return new CommandParameter { Name = x.Identifier.Text, @@ -172,7 +171,8 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta IsCancellationToken = isCancellationToken, IsFromServices = isFromServices, Aliases = [], - Description = "" + Description = "", + ArgumentIndex = argumentIndex, }; }) .Where(x => x.Type != null) @@ -214,23 +214,38 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta { isVoid = false; } + else if ((methodSymbol.ReturnType as INamedTypeSymbol)!.EqualsUnconstructedGenericType(wellKnownTypes.Task)) + { + isVoid = true; + isAsync = true; + } + else if ((methodSymbol.ReturnType as INamedTypeSymbol)!.EqualsUnconstructedGenericType(wellKnownTypes.Task_T)) + { + var typeArg = (methodSymbol.ReturnType as INamedTypeSymbol)!.TypeArguments[0]; + if (typeArg.SpecialType == SpecialType.System_Int32) + { + isVoid = false; + isAsync = true; + } + else + { + // TODO: invalid return + return null; + } + } + else + { + // TODO: invalid return type + return null; + } - // TODO: check for Task, Task - //methodSymbol.ReturnType.SpecialType == SpecialType.System_Threading_Tasks_Task_T - var task = methodSymbol.ReturnType.Name == "Task"; - + var parsableIndex = 0; var parameters = methodSymbol.Parameters .Select(x => { - var parserAttr = x.GetAttributes().FirstOrDefault(x => x.AttributeClass?.Name == "ParserAttribute"); - - if (parserAttr != null) - { - // TODO: get parser - } - - // TODO: check FromServcies Attribute - + var customParserType = x.GetAttributes().FirstOrDefault(x => x.AttributeClass?.AllInterfaces.Any(y => y.Name == "IArgumentParser") ?? false); + var hasFromServices = x.GetAttributes().Any(x => x.AttributeClass?.Name == "FromServicesAttribute"); + var hasArgument = x.GetAttributes().Any(x => x.AttributeClass?.Name == "ArgumentAttribute"); var isCancellationToken = SymbolEqualityComparer.Default.Equals(x.Type, wellKnownTypes.CancellationToken); string description = ""; @@ -240,6 +255,19 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta ParseParameterDescription(desc, out aliases, out description); } + var argumentIndex = -1; + if (!(hasFromServices || isCancellationToken)) + { + if (hasArgument) + { + argumentIndex = parsableIndex++; + } + else + { + parsableIndex++; + } + } + return new CommandParameter { Name = x.Name, @@ -248,8 +276,9 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta DefaultValue = x.HasExplicitDefaultValue ? x.ExplicitDefaultValue : null, CustomParserType = null, IsCancellationToken = isCancellationToken, - IsFromServices = false, + IsFromServices = hasFromServices, Aliases = aliases, + ArgumentIndex = argumentIndex, Description = description }; }) diff --git a/src/ConsoleAppFramework5/WellKnownTypes.cs b/src/ConsoleAppFramework5/WellKnownTypes.cs index 6523ded..2442a0f 100644 --- a/src/ConsoleAppFramework5/WellKnownTypes.cs +++ b/src/ConsoleAppFramework5/WellKnownTypes.cs @@ -13,17 +13,17 @@ public class WellKnownTypes(Compilation compilation) INamedTypeSymbol? version; public INamedTypeSymbol Version => version ??= GetTypeByMetadataName("System.Version"); - INamedTypeSymbol? parsable; - public INamedTypeSymbol? IParsable => parsable ??= compilation.GetTypeByMetadataName("System.IParsable`1"); + INamedTypeSymbol? spanParsable; + public INamedTypeSymbol? ISpanParsable => spanParsable ??= compilation.GetTypeByMetadataName("System.ISpanParsable`1"); INamedTypeSymbol? cancellationToken; - public INamedTypeSymbol? CancellationToken => cancellationToken ??= compilation.GetTypeByMetadataName("System.Threading.CancellationToken"); + public INamedTypeSymbol CancellationToken => cancellationToken ??= GetTypeByMetadataName("System.Threading.CancellationToken"); INamedTypeSymbol? task; - public INamedTypeSymbol? Task => task ??= compilation.GetTypeByMetadataName("System.Threading.Tasks.Task"); + public INamedTypeSymbol Task => task ??= GetTypeByMetadataName("System.Threading.Tasks.Task"); INamedTypeSymbol? task_T; - public INamedTypeSymbol? Task_T => task_T ??= compilation.GetTypeByMetadataName("System.Threading.Tasks.Task`1"); + public INamedTypeSymbol Task_T => task_T ??= GetTypeByMetadataName("System.Threading.Tasks.Task`1"); public bool HasTryParse(ITypeSymbol type) { From acce413862a2a03aecfe12ef7e6f80622e8c3be1 Mon Sep 17 00:00:00 2001 From: neuecc Date: Wed, 15 May 2024 02:45:26 +0900 Subject: [PATCH 06/54] array parse --- sandbox/GeneratorSandbox/Program.cs | 118 +++++++++++++++++- src/ConsoleAppFramework5/Command.cs | 15 +++ .../ConsoleAppGenerator.cs | 54 +++++++- 3 files changed, 177 insertions(+), 10 deletions(-) diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index c5e437b..60e2f60 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Data; using System.Diagnostics.CodeAnalysis; using System.Numerics; using System.Runtime.CompilerServices; @@ -9,9 +10,9 @@ using System.Threading.Tasks; using ConsoleAppFramework; using Microsoft.Extensions.DependencyInjection; -using Takoyaki; -args = ["100", "--y", "20"]; // test. + +args = ["100", "--y", "1,10,100,100,1000"]; // test. // var s = "foo"; @@ -34,9 +35,9 @@ //ConsoleApp.Run(args, ([Vector3Parser] Vector3 x) => //{Quaternion// //}); - ConsoleApp.Run(args, ([Argument] int x, int y) => + ConsoleApp.Run(args, ([Argument] int x, int[] y) => { - Console.WriteLine(x + y); + Console.WriteLine((x, y)); }); @@ -51,6 +52,10 @@ ConsoleApp.ServiceProvider = provider; + + + + static async Task RunRun([Vector3Parser] Vector3 x, [FromServices] MyClass y) { Console.WriteLine("Hello World!" + x + y); @@ -153,6 +158,55 @@ public static bool TryParse(ReadOnlySpan s, out Vector3 result) } } + + +[AttributeUsage(AttributeTargets.Parameter)] +internal sealed class ArrayParserAttribute : Attribute, IArgumentParser + where T : ISpanParsable +{ + public static bool TryParse(ReadOnlySpan s, out T[] result) + { + 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(',')) // check have more region + { + 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; + } +} + + namespace ConsoleAppFramework { //partial class ConsoleApp @@ -190,6 +244,61 @@ namespace ConsoleAppFramework partial class ConsoleApp { + static bool TrySplitParse2(ReadOnlySpan s, out T[] result) + where T : ISpanParsable + { + if (s.StartsWith("[")) + { + try + { + result = System.Text.Json.JsonSerializer.Deserialize(s)!; + } + 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; + } + public static void Run2(string[] args, Action command) { var arg0 = default(int); @@ -256,3 +365,4 @@ public static void Run2(string[] args, Action command) } } + diff --git a/src/ConsoleAppFramework5/Command.cs b/src/ConsoleAppFramework5/Command.cs index 67c99fb..2b30a67 100644 --- a/src/ConsoleAppFramework5/Command.cs +++ b/src/ConsoleAppFramework5/Command.cs @@ -176,6 +176,21 @@ public string BuildParseMethod(int argCount, string argumentName, WellKnownTypes return $"if (!Enum.TryParse<{Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>(args[{index}], true, out arg{argCount})) ThrowArgumentParseFailed(\"{argumentName}\", args[i]);"; } + // Array + if (Type.TypeKind == TypeKind.Array) + { + var elementType = (Type as IArrayTypeSymbol)!.ElementType; + var parsable = wellKnownTypes.ISpanParsable; + if (parsable != null) // has parsable + { + if (elementType.AllInterfaces.Any(x => x.EqualsUnconstructedGenericType(parsable))) + { + return $"if (!TrySplitParse(args[{index}], out arg{argCount})) ThrowArgumentParseFailed(\"{argumentName}\", args[i]);"; + } + } + break; + } + // System.DateTimeOffset, System.Guid, System.Version tryParseKnownPrimitive = wellKnownTypes.HasTryParse(Type); diff --git a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs index 1a7319c..a1b8ea9 100644 --- a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs @@ -92,17 +92,59 @@ static void ThrowArgumentNameNotFound(string argumentName) throw new ArgumentException($"Argument '{argumentName}' does not found in command prameters."); } - static System.Collections.Generic.IEnumerable<(int start, int end)> Split(string str) + static bool TrySplitParse(ReadOnlySpan s, out T[] result) + where T : ISpanParsable { - var start = 0; - for (var i = 0; i < str.Length; i++) + if (s.StartsWith("[")) { - if (str[i] == ',') + try { - yield return (start, i - 1); - start = i + 1; + result = System.Text.Json.JsonSerializer.Deserialize(s)!; + } + 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; } sealed class PosixSignalHandler : IDisposable From 0bc24d347c67d7d8ad495c3fa980b7634a622fb6 Mon Sep 17 00:00:00 2001 From: neuecc Date: Wed, 15 May 2024 18:47:50 +0900 Subject: [PATCH 07/54] mores --- ConsoleAppFramework.sln | 7 + sandbox/GeneratorSandbox/Program.cs | 110 ++++++------ src/ConsoleAppFramework5/Command.cs | 165 ++++++++++-------- .../ConsoleAppGenerator.cs | 70 +++++++- src/ConsoleAppFramework5/Emitter.cs | 4 + src/ConsoleAppFramework5/Parser.cs | 15 +- .../ConsoleAppFramework.GeneratorTests.csproj | 25 +++ .../GlobalUsings.cs | 1 + .../UnitTest1.cs | 11 ++ 9 files changed, 285 insertions(+), 123 deletions(-) create mode 100644 tests/ConsoleAppFramework.GeneratorTests/ConsoleAppFramework.GeneratorTests.csproj create mode 100644 tests/ConsoleAppFramework.GeneratorTests/GlobalUsings.cs create mode 100644 tests/ConsoleAppFramework.GeneratorTests/UnitTest1.cs diff --git a/ConsoleAppFramework.sln b/ConsoleAppFramework.sln index deb796d..a33e143 100644 --- a/ConsoleAppFramework.sln +++ b/ConsoleAppFramework.sln @@ -38,6 +38,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GeneratorSandbox", "sandbox EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFrameworkBenchmark", "sandbox\CliFrameworkBenchmark\CliFrameworkBenchmark.csproj", "{F558E4F2-1AB0-4634-B613-69DFE79894AF}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleAppFramework.GeneratorTests", "tests\ConsoleAppFramework.GeneratorTests\ConsoleAppFramework.GeneratorTests.csproj", "{C54F7FE8-650A-4DC7-877F-0DE929351800}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -84,6 +86,10 @@ Global {F558E4F2-1AB0-4634-B613-69DFE79894AF}.Debug|Any CPU.Build.0 = Debug|Any CPU {F558E4F2-1AB0-4634-B613-69DFE79894AF}.Release|Any CPU.ActiveCfg = Release|Any CPU {F558E4F2-1AB0-4634-B613-69DFE79894AF}.Release|Any CPU.Build.0 = Release|Any CPU + {C54F7FE8-650A-4DC7-877F-0DE929351800}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C54F7FE8-650A-4DC7-877F-0DE929351800}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C54F7FE8-650A-4DC7-877F-0DE929351800}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C54F7FE8-650A-4DC7-877F-0DE929351800}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -99,6 +105,7 @@ Global {09BEEA7B-B6D3-4011-BCAB-6DF976713695} = {1F399F98-7439-4F05-847B-CC1267B4B7F2} {ACDA48BA-0BFE-4917-B335-7836DAA5929A} = {A2CF2984-E8E2-48FC-B5A1-58D74A2467E6} {F558E4F2-1AB0-4634-B613-69DFE79894AF} = {A2CF2984-E8E2-48FC-B5A1-58D74A2467E6} + {C54F7FE8-650A-4DC7-877F-0DE929351800} = {AAD2D900-C305-4449-A9FC-6C7696FFEDFA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7F3E353A-C125-4020-8481-11DC6496358C} diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index 60e2f60..fffc9e0 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -1,10 +1,14 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Data; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Numerics; +using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -12,7 +16,9 @@ using Microsoft.Extensions.DependencyInjection; -args = ["100", "--y", "1,10,100,100,1000"]; // test. +args = ["--x", "100", "--y", "1000"]; // test. + + // var s = "foo"; @@ -29,18 +35,44 @@ // --x + + + + unsafe { + ConsoleApp.LogError = Console.Error.WriteLine; + + // GetValidationResult + + + + + //ConsoleApp.Run(args, ([Vector3Parser] Vector3 x) => //{Quaternion// //}); - ConsoleApp.Run(args, ([Argument] int x, int[] y) => + ConsoleApp.Run(args, ([Range(1, 10)] int x, [Range(100, 2000)] int y) => { - Console.WriteLine((x, y)); + + + + var m = MethodInfo.GetCurrentMethod(); + var parameters = m!.GetParameters(); + var context = new ValidationContext("", null, null); + StringBuilder? sb = null; + + + ConsoleApp.ValidateParameter(x, context, ref sb, parameters, 0); + ConsoleApp.ValidateParameter(y, context, ref sb, parameters, 1); + + // throw new ValidationException + }); + } // description @@ -56,7 +88,7 @@ -static async Task RunRun([Vector3Parser] Vector3 x, [FromServices] MyClass y) +static async Task RunRun(int? x = null, string? y = null) { Console.WriteLine("Hello World!" + x + y); return 0; @@ -242,65 +274,45 @@ namespace ConsoleAppFramework //} partial class ConsoleApp - { - static bool TrySplitParse2(ReadOnlySpan s, out T[] result) - where T : ISpanParsable + public static void ValidateParameter(object? value, ValidationContext validationContext, ref StringBuilder? errorMessages, ParameterInfo[] parameters, int index) { - if (s.StartsWith("[")) + var p = parameters[index]; + validationContext.DisplayName = p.Name ?? ""; + validationContext.Items.Clear(); + + foreach (var validator in p.GetCustomAttributes(false)) { - try + var result = validator.GetValidationResult(value, validationContext); + if (result != null) { - result = System.Text.Json.JsonSerializer.Deserialize(s)!; - } - catch - { - result = default!; - return false; + if (errorMessages == null) + { + errorMessages = new StringBuilder(); + } + errorMessages.AppendLine(result.ErrorMessage); } } + } - var count = s.Count(',') + 1; - result = new T[count]; - - var source = s; - var destination = result.AsSpan(); - Span ranges = stackalloc Range[Math.Min(count, 128)]; + // [MethodImpl + public static void Run2(string[] args, Action command) + { - while (true) + var parameters = command.GetMethodInfo().GetParameters(); { - var splitCount = source.Split(ranges, ','); - var parseTo = splitCount; - if (splitCount == 128 && source[ranges[^1]].Contains(',')) - { - parseTo = splitCount - 1; - } + var validationContext = new ValidationContext(1000, null, null); + // parameters[0].GetCustomAttributes(false).Select(x => x.Validate( - 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; - } - public static void Run2(string[] args, Action command) - { + + + + if (TryShowHelpOrVersion(args)) return; + var arg0 = default(int); var arg0Parsed = false; var arg1 = default(int); diff --git a/src/ConsoleAppFramework5/Command.cs b/src/ConsoleAppFramework5/Command.cs index 2b30a67..54dabe8 100644 --- a/src/ConsoleAppFramework5/Command.cs +++ b/src/ConsoleAppFramework5/Command.cs @@ -42,7 +42,7 @@ public string BuildDelegateSignature(out string? delegateType) } else { - var parameters = string.Join(", ", Parameters.Select(x => x.Type.ToFullyQualifiedFormatDisplayString())); + var parameters = string.Join(", ", Parameters.Select(x => x.ToTypeDisplayString())); return $"Func<{parameters}, Task>"; } } @@ -55,7 +55,7 @@ public string BuildDelegateSignature(out string? delegateType) } else { - var parameters = string.Join(", ", Parameters.Select(x => x.Type.ToFullyQualifiedFormatDisplayString())); + var parameters = string.Join(", ", Parameters.Select(x => x.ToTypeDisplayString())); return $"Func<{parameters}, Task>"; } } @@ -71,7 +71,7 @@ public string BuildDelegateSignature(out string? delegateType) } else { - var parameters = string.Join(", ", Parameters.Select(x => x.Type.ToFullyQualifiedFormatDisplayString())); + var parameters = string.Join(", ", Parameters.Select(x => x.ToTypeDisplayString())); return $"Action<{parameters}>"; } } @@ -84,7 +84,7 @@ public string BuildDelegateSignature(out string? delegateType) } else { - var parameters = string.Join(", ", Parameters.Select(x => x.Type.ToFullyQualifiedFormatDisplayString())); + var parameters = string.Join(", ", Parameters.Select(x => x.ToTypeDisplayString())); return $"Func<{parameters}, int>"; } } @@ -101,7 +101,7 @@ public string BuildFunctionPointerDelegateSignature() (false, false) => "int" }; - var parameters = string.Join(", ", Parameters.Select(x => x.Type.ToFullyQualifiedFormatDisplayString())); + var parameters = string.Join(", ", Parameters.Select(x => x.ToTypeDisplayString())); var comma = Parameters.Length > 0 ? ", " : ""; return $"delegate* managed<{parameters}{comma}{retType}>"; } @@ -124,6 +124,7 @@ public string BuildDelegateType(string delegateName) public record class CommandParameter { public required ITypeSymbol Type { get; init; } + public required bool IsNullableReference { get; init; } public required string Name { get; init; } public required bool HasDefaultValue { get; init; } public object? DefaultValue { get; init; } @@ -139,85 +140,99 @@ public record class CommandParameter public string BuildParseMethod(int argCount, string argumentName, WellKnownTypes wellKnownTypes, bool increment) { var index = increment ? "++i" : "i"; + return Core(Type, false); - if (CustomParserType != null) + string Core(ITypeSymbol type, bool nullable) { - return $"if (!{CustomParserType.ToFullyQualifiedFormatDisplayString()}.TryParse(args[{index}], out arg{argCount})) ThrowArgumentParseFailed(\"{argumentName}\", args[i]);"; - } + var tryParseKnownPrimitive = false; + var tryParseIParsable = false; - var tryParseKnownPrimitive = false; - var tryParseIParsable = false; + var outArgVar = (!nullable) ? $"out arg{argCount}" : $"out var temp{argCount}"; + var elseExpr = (!nullable) ? "" : $" else {{ arg{argCount} = temp{argCount}; }}"; - switch (Type.SpecialType) - { - case SpecialType.System_String: - return $"arg{argCount} = args[{index}];"; // no parse - case SpecialType.System_Boolean: - return $"arg{argCount} = true;"; // bool is true flag - case SpecialType.System_Char: - case SpecialType.System_SByte: - case SpecialType.System_Byte: - case SpecialType.System_Int16: - case SpecialType.System_UInt16: - case SpecialType.System_Int32: - case SpecialType.System_UInt32: - case SpecialType.System_Int64: - case SpecialType.System_UInt64: - case SpecialType.System_Decimal: - case SpecialType.System_Single: - case SpecialType.System_Double: - case SpecialType.System_DateTime: - tryParseKnownPrimitive = true; - break; - default: - // Enum - if (Type.TypeKind == TypeKind.Enum) - { - return $"if (!Enum.TryParse<{Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>(args[{index}], true, out arg{argCount})) ThrowArgumentParseFailed(\"{argumentName}\", args[i]);"; - } + // Nullable + if (type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) + { + var valueType = (type as INamedTypeSymbol)!.TypeArguments[0]; + return Core(valueType, true); + } - // Array - if (Type.TypeKind == TypeKind.Array) - { - var elementType = (Type as IArrayTypeSymbol)!.ElementType; - var parsable = wellKnownTypes.ISpanParsable; - if (parsable != null) // has parsable + if (CustomParserType != null) + { + return $"if (!{CustomParserType.ToFullyQualifiedFormatDisplayString()}.TryParse(args[{index}], {outArgVar})) {{ ThrowArgumentParseFailed(\"{argumentName}\", args[i]); }}{elseExpr}"; + } + + switch (type.SpecialType) + { + case SpecialType.System_String: + return $"arg{argCount} = args[{index}];"; // no parse + case SpecialType.System_Boolean: + return $"arg{argCount} = true;"; // bool is true flag + case SpecialType.System_Char: + case SpecialType.System_SByte: + case SpecialType.System_Byte: + case SpecialType.System_Int16: + case SpecialType.System_UInt16: + case SpecialType.System_Int32: + case SpecialType.System_UInt32: + case SpecialType.System_Int64: + case SpecialType.System_UInt64: + case SpecialType.System_Decimal: + case SpecialType.System_Single: + case SpecialType.System_Double: + case SpecialType.System_DateTime: + tryParseKnownPrimitive = true; + break; + default: + // Enum + if (type.TypeKind == TypeKind.Enum) + { + return $"if (!Enum.TryParse<{type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>(args[{index}], true, {outArgVar})) {{ ThrowArgumentParseFailed(\"{argumentName}\", args[i]); }}{elseExpr}"; + } + + // Array + if (type.TypeKind == TypeKind.Array) { - if (elementType.AllInterfaces.Any(x => x.EqualsUnconstructedGenericType(parsable))) + var elementType = (type as IArrayTypeSymbol)!.ElementType; + var parsable = wellKnownTypes.ISpanParsable; + if (parsable != null) // has parsable { - return $"if (!TrySplitParse(args[{index}], out arg{argCount})) ThrowArgumentParseFailed(\"{argumentName}\", args[i]);"; + if (elementType.AllInterfaces.Any(x => x.EqualsUnconstructedGenericType(parsable))) + { + return $"if (!TrySplitParse(args[{index}], {outArgVar})) {{ ThrowArgumentParseFailed(\"{argumentName}\", args[i]); }}{elseExpr}"; + } } + break; } - break; - } - // System.DateTimeOffset, System.Guid, System.Version - tryParseKnownPrimitive = wellKnownTypes.HasTryParse(Type); + // System.DateTimeOffset, System.Guid, System.Version + tryParseKnownPrimitive = wellKnownTypes.HasTryParse(type); - if (!tryParseKnownPrimitive) - { - // ISpanParsable (BigInteger, Complex, Half, Int128, etc...) - var parsable = wellKnownTypes.ISpanParsable; - if (parsable != null) // has parsable + if (!tryParseKnownPrimitive) { - tryParseIParsable = Type.AllInterfaces.Any(x => x.EqualsUnconstructedGenericType(parsable)); + // ISpanParsable (BigInteger, Complex, Half, Int128, etc...) + var parsable = wellKnownTypes.ISpanParsable; + if (parsable != null) // has parsable + { + tryParseIParsable = type.AllInterfaces.Any(x => x.EqualsUnconstructedGenericType(parsable)); + } } - } - break; - } + break; + } - if (tryParseKnownPrimitive) - { - return $"if (!{Type.ToFullyQualifiedFormatDisplayString()}.TryParse(args[{index}], out arg{argCount})) ThrowArgumentParseFailed(\"{argumentName}\", args[i]);"; - } - else if (tryParseIParsable) - { - return $"if (!{Type.ToFullyQualifiedFormatDisplayString()}.TryParse(args[{index}], null, out arg{argCount})) ThrowArgumentParseFailed(\"{argumentName}\", args[i]);"; - } - else - { - return $"try {{ arg{argCount} = System.Text.Json.JsonSerializer.Deserialize<{Type.ToFullyQualifiedFormatDisplayString()}>(args[{index}]); }} catch {{ ThrowArgumentParseFailed(\"{argumentName}\", args[i]); }}"; + if (tryParseKnownPrimitive) + { + return $"if (!{type.ToFullyQualifiedFormatDisplayString()}.TryParse(args[{index}], {outArgVar})) {{ ThrowArgumentParseFailed(\"{argumentName}\", args[i]); }}{elseExpr}"; + } + else if (tryParseIParsable) + { + return $"if (!{type.ToFullyQualifiedFormatDisplayString()}.TryParse(args[{index}], null, {outArgVar})) {{ ThrowArgumentParseFailed(\"{argumentName}\", args[i]); }}{elseExpr}"; + } + else + { + return $"try {{ arg{argCount} = System.Text.Json.JsonSerializer.Deserialize<{type.ToFullyQualifiedFormatDisplayString()}>(args[{index}]); }} catch {{ ThrowArgumentParseFailed(\"{argumentName}\", args[i]); }}"; + } } } @@ -233,18 +248,24 @@ public string DefaultValueToString(bool castValue = true) } if (DefaultValue == null) { - if (!castValue) return "null"; - return $"({Type.ToFullyQualifiedFormatDisplayString()})null"; + // null -> default(T) to support both class and struct + return $"default({Type.ToFullyQualifiedFormatDisplayString()})"; } if (!castValue) return DefaultValue.ToString(); return $"({Type.ToFullyQualifiedFormatDisplayString()}){DefaultValue}"; } + public string ToTypeDisplayString() + { + var t = Type.ToFullyQualifiedFormatDisplayString(); + return IsNullableReference ? $"{t}?" : t; + } + public override string ToString() { var sb = new StringBuilder(); - sb.Append(Type.ToFullyQualifiedFormatDisplayString()); + sb.Append(ToTypeDisplayString()); sb.Append(" "); sb.Append(Name); if (HasDefaultValue) diff --git a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs index a1b8ea9..aeb56ef 100644 --- a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs @@ -43,8 +43,10 @@ static void EmitConsoleAppTemplateSource(IncrementalGeneratorPostInitializationC namespace ConsoleAppFramework; using System; +using System.Reflection; using System.Threading.Tasks; using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; using System.Diagnostics.CodeAnalysis; internal interface IArgumentParser @@ -64,10 +66,23 @@ internal sealed class ArgumentAttribute : Attribute internal static partial class ConsoleApp { - public static Action LogError { get; set; } = msg => Console.WriteLine(msg); public static IServiceProvider? ServiceProvider { get; set; } public static TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(5); + static Action? logAction; + public static Action Log + { + get => logAction ??= Console.WriteLine; + set => logAction = value; + } + + static Action? logErrorAction; + public static Action LogError + { + get => logErrorAction ??= Console.WriteLine; + set => logErrorAction = value; + } + public static void Run(string[] args) { } @@ -147,6 +162,59 @@ static bool TrySplitParse(ReadOnlySpan s, out T[] result) return true; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static bool TryShowHelpOrVersion(string[] args) + { + if (args.Length == 0) + { + ShowHelp(); // TODO: if no args root command, return false. + return true; + } + + if (args.Length == 1) + { + switch (args[0]) + { + case "--version": + ShowVersion(); + return true; + case "-h": + case "--help": + ShowHelp(); + return true; + default: + break; + } + } + + return false; + } + + static void ShowVersion() + { + 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 void ShowHelp() + { + Log("TODO: Build Help"); + } + sealed class PosixSignalHandler : IDisposable { public CancellationToken Token => cancellationTokenSource.Token; diff --git a/src/ConsoleAppFramework5/Emitter.cs b/src/ConsoleAppFramework5/Emitter.cs index 7985850..bf1a61d 100644 --- a/src/ConsoleAppFramework5/Emitter.cs +++ b/src/ConsoleAppFramework5/Emitter.cs @@ -71,12 +71,14 @@ public string EmitRun(bool isRunAsync) { fastParseCase.AppendLine($" case \"{alias}\":"); } + fastParseCase.AppendLine(" {"); fastParseCase.AppendLine($" {parameter.BuildParseMethod(i, parameter.Name, wellKnownTypes, increment: true)}"); if (!parameter.HasDefaultValue) { fastParseCase.AppendLine($" arg{i}Parsed = true;"); } fastParseCase.AppendLine(" break;"); + fastParseCase.AppendLine(" }"); } // parse argument(slow, if ignorecase) -> @@ -162,6 +164,8 @@ public string EmitRun(bool isRunAsync) var code = $$""" public static {{unsafeCode}}{{returnType}} {{methodName}}(string[] args, {{commandMethodType}} command) { + if (TryShowHelpOrVersion(args)) return; + {{prepareArgument}} try { diff --git a/src/ConsoleAppFramework5/Parser.cs b/src/ConsoleAppFramework5/Parser.cs index dc93199..4f126a6 100644 --- a/src/ConsoleAppFramework5/Parser.cs +++ b/src/ConsoleAppFramework5/Parser.cs @@ -98,7 +98,14 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta if (x.Default?.Value is LiteralExpressionSyntax literal) { var token = literal.Token; - defaultValue = token.Value; + if (token.IsKind(SyntaxKind.DefaultKeyword)) + { + defaultValue = null; + } + else + { + defaultValue = token.Value; + } } // bool is always optional flag @@ -161,9 +168,12 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta } } + var isNullableReference = x.Type.IsKind(SyntaxKind.NullableType) && type.Type?.OriginalDefinition.SpecialType != SpecialType.System_Nullable_T; + return new CommandParameter { Name = x.Identifier.Text, + IsNullableReference = isNullableReference, Type = type.Type!, HasDefaultValue = hasDefault, DefaultValue = defaultValue, @@ -268,9 +278,12 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta } } + var isNullableReference = x.NullableAnnotation == NullableAnnotation.Annotated && x.Type.OriginalDefinition.SpecialType != SpecialType.System_Nullable_T; + return new CommandParameter { Name = x.Name, + IsNullableReference = isNullableReference, Type = x.Type, HasDefaultValue = x.HasExplicitDefaultValue, DefaultValue = x.HasExplicitDefaultValue ? x.ExplicitDefaultValue : null, diff --git a/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppFramework.GeneratorTests.csproj b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppFramework.GeneratorTests.csproj new file mode 100644 index 0000000..9e0c306 --- /dev/null +++ b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppFramework.GeneratorTests.csproj @@ -0,0 +1,25 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + diff --git a/tests/ConsoleAppFramework.GeneratorTests/GlobalUsings.cs b/tests/ConsoleAppFramework.GeneratorTests/GlobalUsings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/tests/ConsoleAppFramework.GeneratorTests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/tests/ConsoleAppFramework.GeneratorTests/UnitTest1.cs b/tests/ConsoleAppFramework.GeneratorTests/UnitTest1.cs new file mode 100644 index 0000000..9d48d68 --- /dev/null +++ b/tests/ConsoleAppFramework.GeneratorTests/UnitTest1.cs @@ -0,0 +1,11 @@ +namespace ConsoleAppFramework.GeneratorTests +{ + public class UnitTest1 + { + [Fact] + public void Test1() + { + + } + } +} \ No newline at end of file From a3cc460406ceb4475c39ba073765b847d76da4fc Mon Sep 17 00:00:00 2001 From: neuecc Date: Fri, 17 May 2024 10:38:31 +0900 Subject: [PATCH 08/54] impl validation --- sandbox/GeneratorSandbox/Program.cs | 84 +++++++++---------- src/ConsoleAppFramework5/Command.cs | 4 + .../ConsoleAppGenerator.cs | 21 +++++ src/ConsoleAppFramework5/Emitter.cs | 31 ++++++- src/ConsoleAppFramework5/Parser.cs | 14 ++++ src/ConsoleAppFramework5/RoslynExtensions.cs | 11 +++ 6 files changed, 121 insertions(+), 44 deletions(-) diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index fffc9e0..aac445f 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis; using System.Numerics; using System.Reflection; +using System.Reflection.Metadata; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; @@ -16,7 +17,7 @@ using Microsoft.Extensions.DependencyInjection; -args = ["--x", "100", "--y", "1000"]; // test. +args = ["--x", "18", "--y", "aiueokakikukeko"]; // test. @@ -53,21 +54,11 @@ //ConsoleApp.Run(args, ([Vector3Parser] Vector3 x) => //{Quaternion// //}); - ConsoleApp.Run(args, ([Range(1, 10)] int x, [Range(100, 2000)] int y) => + ConsoleApp.Run(args, ([Range(1, 10)] int x, [StringLength(10)] string y) => { - + Console.WriteLine("OK"); - var m = MethodInfo.GetCurrentMethod(); - var parameters = m!.GetParameters(); - var context = new ValidationContext("", null, null); - StringBuilder? sb = null; - - - ConsoleApp.ValidateParameter(x, context, ref sb, parameters, 0); - ConsoleApp.ValidateParameter(y, context, ref sb, parameters, 1); - - // throw new ValidationException }); @@ -275,40 +266,31 @@ namespace ConsoleAppFramework partial class ConsoleApp { - public static void ValidateParameter(object? value, ValidationContext validationContext, ref StringBuilder? errorMessages, ParameterInfo[] parameters, int index) - { - var p = parameters[index]; - validationContext.DisplayName = p.Name ?? ""; - validationContext.Items.Clear(); - - foreach (var validator in p.GetCustomAttributes(false)) - { - var result = validator.GetValidationResult(value, validationContext); - if (result != null) - { - if (errorMessages == null) - { - errorMessages = new StringBuilder(); - } - errorMessages.AppendLine(result.ErrorMessage); - } - } - } + //public 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 public static void Run2(string[] args, Action command) { - var parameters = command.GetMethodInfo().GetParameters(); - { - var validationContext = new ValidationContext(1000, null, null); - // parameters[0].GetCustomAttributes(false).Select(x => x.Validate( - - - } - - + // command.Method if (TryShowHelpOrVersion(args)) return; @@ -365,13 +347,29 @@ public static void Run2(string[] args, Action command) if (!arg0Parsed) ThrowRequiredArgumentNotParsed("x"); if (!arg1Parsed) ThrowRequiredArgumentNotParsed("y"); + var validationContext = new ValidationContext(1000, null, null); + var parameters = command.GetMethodInfo().GetParameters(); + StringBuilder? errorMessages = null; + ValidateParameter(arg0, parameters[0], validationContext, ref errorMessages); + ValidateParameter(arg1, parameters[1], validationContext, ref errorMessages); + if (errorMessages != null) + { + throw new ValidationException(errorMessages.ToString()); + } + command(arg0!, arg1!); } - catch (Exception ex) { Environment.ExitCode = 1; - LogError(ex.ToString()); + if (ex is ValidationException ve) + { + LogError(ex.Message); + } + else + { + LogError(ex.ToString()); + } } } } diff --git a/src/ConsoleAppFramework5/Command.cs b/src/ConsoleAppFramework5/Command.cs index 54dabe8..53a75f6 100644 --- a/src/ConsoleAppFramework5/Command.cs +++ b/src/ConsoleAppFramework5/Command.cs @@ -132,6 +132,10 @@ public record class CommandParameter public required bool IsFromServices { get; init; } public required bool IsCancellationToken { get; init; } public bool IsParsable => !(IsFromServices || IsCancellationToken); + + // 追加!コンパイルエラーありがたい! + public required bool HasValidation { get; init; } + public required int ArgumentIndex { get; init; } // -1 is not Argument, other than marked as [Argument] public bool IsArgument => ArgumentIndex != -1; public required string[] Aliases { get; init; } diff --git a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs index aeb56ef..03b6e87 100644 --- a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs @@ -43,11 +43,13 @@ static void EmitConsoleAppTemplateSource(IncrementalGeneratorPostInitializationC namespace ConsoleAppFramework; using System; +using System.Text; using System.Reflection; using System.Threading.Tasks; using System.Runtime.InteropServices; using System.Runtime.CompilerServices; using System.Diagnostics.CodeAnalysis; +using System.ComponentModel.DataAnnotations; internal interface IArgumentParser { @@ -162,6 +164,25 @@ static bool TrySplitParse(ReadOnlySpan s, out T[] result) 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(string[] args) { diff --git a/src/ConsoleAppFramework5/Emitter.cs b/src/ConsoleAppFramework5/Emitter.cs index bf1a61d..8fcb334 100644 --- a/src/ConsoleAppFramework5/Emitter.cs +++ b/src/ConsoleAppFramework5/Emitter.cs @@ -10,6 +10,7 @@ public string EmitRun(bool isRunAsync) { var hasCancellationToken = command.Parameters.Any(x => x.IsCancellationToken); var hasArgument = command.Parameters.Any(x => x.IsArgument); + var hasValidation = command.Parameters.Any(x => x.HasValidation); // prepare argument variables -> var prepareArgument = new StringBuilder(); @@ -118,6 +119,26 @@ public string EmitRun(bool isRunAsync) } } + // hasValidation -> + var attributeValidation = new StringBuilder(); + if (hasValidation) + { + attributeValidation.AppendLine(" var validationContext = new System.ComponentModel.DataAnnotations.ValidationContext(\"\", null, null);"); + attributeValidation.AppendLine(" var parameters = command.Method.GetParameters();"); + attributeValidation.AppendLine(" System.Text.StringBuilder? errorMessages = null;"); + for (int i = 0; i < command.Parameters.Length; i++) + { + var parameter = command.Parameters[i]; + if (!parameter.HasValidation) continue; + + attributeValidation.AppendLine($" ValidateParameter(arg{i}, parameters[{i}], validationContext, ref errorMessages);"); + } + attributeValidation.AppendLine(" if (errorMessages != null)"); + attributeValidation.AppendLine(" {"); + attributeValidation.AppendLine(" throw new System.ComponentModel.DataAnnotations.ValidationException(errorMessages.ToString());"); + attributeValidation.AppendLine(" }"); + } + // invoke for sync/async, void/int var methodArguments = string.Join(", ", command.Parameters.Select((x, i) => $"arg{i}!")); var invokeCommand = $"command({methodArguments})"; @@ -184,11 +205,19 @@ public string EmitRun(bool isRunAsync) } } {{validateParsed}} +{{attributeValidation}} {{invoke}} catch (Exception ex) { Environment.ExitCode = 1; - LogError(ex.ToString()); + if (ex is System.ComponentModel.DataAnnotations.ValidationException) + { + LogError(ex.Message); + } + else + { + LogError(ex.ToString()); + } } } """; diff --git a/src/ConsoleAppFramework5/Parser.cs b/src/ConsoleAppFramework5/Parser.cs index 4f126a6..2e5d6bd 100644 --- a/src/ConsoleAppFramework5/Parser.cs +++ b/src/ConsoleAppFramework5/Parser.cs @@ -127,6 +127,17 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta }) .FirstOrDefault(x => x != null); + var hasValidation = x.AttributeLists.SelectMany(x => x.Attributes) + .Any(x => + { + var attr = model.GetTypeInfo(x).Type as INamedTypeSymbol; + if (attr != null && attr.GetBaseTypes().Any(x => x.Name == "ValidationAttribute")) + { + return true; + } + return false; + }); + var isFromServices = x.AttributeLists.SelectMany(x => x.Attributes) .Any(x => { @@ -178,6 +189,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta HasDefaultValue = hasDefault, DefaultValue = defaultValue, CustomParserType = customParserType, + HasValidation = hasValidation, IsCancellationToken = isCancellationToken, IsFromServices = isFromServices, Aliases = [], @@ -256,6 +268,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta var customParserType = x.GetAttributes().FirstOrDefault(x => x.AttributeClass?.AllInterfaces.Any(y => y.Name == "IArgumentParser") ?? false); var hasFromServices = x.GetAttributes().Any(x => x.AttributeClass?.Name == "FromServicesAttribute"); var hasArgument = x.GetAttributes().Any(x => x.AttributeClass?.Name == "ArgumentAttribute"); + var hasValidation = x.GetAttributes().Any(x => x.AttributeClass?.GetBaseTypes().Any(y => y.Name == "ValidationAttribute") ?? false); var isCancellationToken = SymbolEqualityComparer.Default.Equals(x.Type, wellKnownTypes.CancellationToken); string description = ""; @@ -290,6 +303,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta CustomParserType = null, IsCancellationToken = isCancellationToken, IsFromServices = hasFromServices, + HasValidation = hasValidation, Aliases = aliases, ArgumentIndex = argumentIndex, Description = description diff --git a/src/ConsoleAppFramework5/RoslynExtensions.cs b/src/ConsoleAppFramework5/RoslynExtensions.cs index ec56e84..682fc13 100644 --- a/src/ConsoleAppFramework5/RoslynExtensions.cs +++ b/src/ConsoleAppFramework5/RoslynExtensions.cs @@ -18,6 +18,17 @@ public static bool EqualsUnconstructedGenericType(this INamedTypeSymbol left, IN return SymbolEqualityComparer.Default.Equals(l, r); } + public static IEnumerable GetBaseTypes(this INamedTypeSymbol type, bool includeSelf = false) + { + if (includeSelf) yield return type; + var baseType = type.BaseType; + while (baseType != null) + { + yield return baseType; + baseType = baseType.BaseType; + } + } + public static DocumentationCommentTriviaSyntax? GetDocumentationCommentTriviaSyntax(this SyntaxNode node) { // Hack note: From e39c9a9cefceb13efe162490d3d51a5ed964e490 Mon Sep 17 00:00:00 2001 From: neuecc Date: Fri, 17 May 2024 18:34:20 +0900 Subject: [PATCH 09/54] test foundation --- sandbox/GeneratorSandbox/Program.cs | 10 +- .../ConsoleAppGenerator.cs | 5 +- .../CSharpGeneratorRunner.cs | 95 +++++++++++++++++++ .../ConsoleAppFramework.GeneratorTests.csproj | 44 +++++---- .../DiagnosticsTest.cs | 26 +++++ .../GlobalUsings.cs | 3 +- .../RunTest.cs | 48 ++++++++++ .../UnitTest1.cs | 11 --- 8 files changed, 208 insertions(+), 34 deletions(-) create mode 100644 tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs create mode 100644 tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs create mode 100644 tests/ConsoleAppFramework.GeneratorTests/RunTest.cs delete mode 100644 tests/ConsoleAppFramework.GeneratorTests/UnitTest1.cs diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index aac445f..9322a36 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -285,11 +285,19 @@ partial class ConsoleApp // } //} + + static Action? logErrorAction2; + public static Action LogError2 + { + get => logErrorAction2 ??= (static msg => Log(msg)); + set => logErrorAction2 = value; + } + + // [MethodImpl public static void Run2(string[] args, Action command) { - // command.Method diff --git a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs index 03b6e87..4cec8a3 100644 --- a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs @@ -40,11 +40,14 @@ public void Initialize(IncrementalGeneratorInitializationContext context) static void EmitConsoleAppTemplateSource(IncrementalGeneratorPostInitializationContext context) { context.AddSource("ConsoleApp.cs", """ +// +#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; @@ -81,7 +84,7 @@ public static Action Log static Action? logErrorAction; public static Action LogError { - get => logErrorAction ??= Console.WriteLine; + get => logErrorAction ??= (static msg => Log(msg)); set => logErrorAction = value; } diff --git a/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs b/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs new file mode 100644 index 0000000..fef6eff --- /dev/null +++ b/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs @@ -0,0 +1,95 @@ +using ConsoleAppFramework; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.Loader; + +public static class CSharpGeneratorRunner +{ + static Compilation baseCompilation = default!; + + [ModuleInitializer] + public static void InitializeCompilation() + { + // running .NET Core system assemblies dir path + var baseAssemblyPath = Path.GetDirectoryName(typeof(object).Assembly.Location)!; + var systemAssemblies = Directory.GetFiles(baseAssemblyPath) + .Where(x => + { + var fileName = Path.GetFileName(x); + if (fileName.EndsWith("Native.dll")) return false; + return fileName.StartsWith("System") || (fileName is "mscorlib.dll" or "netstandard.dll"); + }); + + var references = systemAssemblies + .Select(x => MetadataReference.CreateFromFile(x)) + .ToArray(); + + var compilation = CSharpCompilation.Create("generatortest", + references: references, + options: new CSharpCompilationOptions(OutputKind.ConsoleApplication)); // .exe + + baseCompilation = compilation; + } + + public static Compilation RunGenerator(string source, string[]? preprocessorSymbols = null, AnalyzerConfigOptionsProvider? options = null) + { + if (preprocessorSymbols == null) + { + preprocessorSymbols = new[] { "NET8_0_OR_GREATER" }; + } + var parseOptions = new CSharpParseOptions(LanguageVersion.CSharp12, preprocessorSymbols: preprocessorSymbols); // 12 + + var driver = CSharpGeneratorDriver.Create(new ConsoleAppGenerator()).WithUpdatedParseOptions(parseOptions); + if (options != null) + { + driver = (Microsoft.CodeAnalysis.CSharp.CSharpGeneratorDriver)driver.WithUpdatedAnalyzerConfigOptions(options); + } + + var compilation = baseCompilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(source, parseOptions)); + + driver.RunGeneratorsAndUpdateCompilation(compilation, out var newCompilation, out var diagnostics); + return newCompilation!; + } + + public static Diagnostic[] RunAndGetErrorDiagnostics(string source, string[]? preprocessorSymbols = null, AnalyzerConfigOptionsProvider? options = null) + { + var compilation = RunGenerator(source, preprocessorSymbols, options); + return compilation.GetDiagnostics().Where(x => x.Severity == DiagnosticSeverity.Error).ToArray(); + } + + public static string CompileAndExecute(string source, string[] args, string[]? preprocessorSymbols = null, AnalyzerConfigOptionsProvider? options = null) + { + var compilation = RunGenerator(source, preprocessorSymbols, options); + + using var ms = new MemoryStream(); + var emitResult = compilation.Emit(ms); + if (!emitResult.Success) + { + throw new InvalidOperationException("Emit Failed\r\n" + string.Join("\r\n", emitResult.Diagnostics.Select(x => x.ToString()))); + } + + ms.Position = 0; + + // capture stdout log + var originalOut = Console.Out; + try + { + var stringWriter = new StringWriter(); + Console.SetOut(stringWriter); + + // load and invoke Main(args) + var loadContext = new AssemblyLoadContext("source-generator", isCollectible: true); + var assembly = loadContext.LoadFromStream(ms); + assembly.EntryPoint!.Invoke(null, new object[] { args }); + loadContext.Unload(); + + return stringWriter.ToString(); + } + finally + { + Console.SetOut(originalOut); + } + } +} \ No newline at end of file diff --git a/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppFramework.GeneratorTests.csproj b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppFramework.GeneratorTests.csproj index 9e0c306..0094e66 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppFramework.GeneratorTests.csproj +++ b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppFramework.GeneratorTests.csproj @@ -1,25 +1,29 @@ - + - - net8.0 - enable - enable + + net8.0 + enable + enable - false - true - + false + true + - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + diff --git a/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs b/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs new file mode 100644 index 0000000..2a07b8b --- /dev/null +++ b/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ConsoleAppFramework.GeneratorTests; + +public class DiagnosticsTest +{ + static void Verify(int id, string code, bool allowMultipleError = false) + { + var diagnostics = CSharpGeneratorRunner.RunAndGetErrorDiagnostics(code); + if (!allowMultipleError) + { + diagnostics.Length.Should().Be(1); + diagnostics[0].Id.Should().Be("CAF" + id.ToString("000")); + } + else + { + diagnostics.Select(x => x.Id).Should().Contain("CAF" + id.ToString("000")); + } + } + + +} diff --git a/tests/ConsoleAppFramework.GeneratorTests/GlobalUsings.cs b/tests/ConsoleAppFramework.GeneratorTests/GlobalUsings.cs index 8c927eb..7fef4b0 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/GlobalUsings.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/GlobalUsings.cs @@ -1 +1,2 @@ -global using Xunit; \ No newline at end of file +global using Xunit; +global using FluentAssertions; \ No newline at end of file diff --git a/tests/ConsoleAppFramework.GeneratorTests/RunTest.cs b/tests/ConsoleAppFramework.GeneratorTests/RunTest.cs new file mode 100644 index 0000000..d5cd3bb --- /dev/null +++ b/tests/ConsoleAppFramework.GeneratorTests/RunTest.cs @@ -0,0 +1,48 @@ +using FluentAssertions; +using Microsoft.CodeAnalysis; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit.Abstractions; + +namespace ConsoleAppFramework.GeneratorTests; + +public class Test // (ITestOutputHelper output) +{ + static void Verify(int id, string code, bool allowMultipleError = false) + { + var diagnostics = CSharpGeneratorRunner.RunAndGetErrorDiagnostics(code); + if (!allowMultipleError) + { + diagnostics.Length.Should().Be(1); + diagnostics[0].Id.Should().Be("CAF" + id.ToString("000")); + } + else + { + diagnostics.Select(x => x.Id).Should().Contain("CAF" + id.ToString("000")); + } + } + + static string[] ToArgs(string args) + { + return args.Split(' '); + } + + [Fact] + public void SyncRun() + { + var result = CSharpGeneratorRunner.CompileAndExecute(""" +using System; +using ConsoleAppFramework; + +ConsoleApp.Run(args, (int x, int y) => { Console.WriteLine((x + y)); }); +""", ToArgs("--x 10 --y 20")); + + result.Should().Be(""" +30 + +"""); + } +} \ No newline at end of file diff --git a/tests/ConsoleAppFramework.GeneratorTests/UnitTest1.cs b/tests/ConsoleAppFramework.GeneratorTests/UnitTest1.cs deleted file mode 100644 index 9d48d68..0000000 --- a/tests/ConsoleAppFramework.GeneratorTests/UnitTest1.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace ConsoleAppFramework.GeneratorTests -{ - public class UnitTest1 - { - [Fact] - public void Test1() - { - - } - } -} \ No newline at end of file From b291b1b5a4f1c362a6cdbc75491c52ae9dc0feb6 Mon Sep 17 00:00:00 2001 From: neuecc Date: Fri, 17 May 2024 20:58:27 +0900 Subject: [PATCH 10/54] verifying --- .../DiagnosticDescriptors.cs | 21 ++++++ src/ConsoleAppFramework5/Parser.cs | 6 ++ ...SharpIncrementalSourceGeneratorVerifier.cs | 34 +++++++++ .../ConsoleAppFramework.GeneratorTests.csproj | 2 +- .../DiagnosticsTest.cs | 35 +++++++++- .../RunTest.cs | 69 ++++++++++++++----- 6 files changed, 146 insertions(+), 21 deletions(-) create mode 100644 src/ConsoleAppFramework5/DiagnosticDescriptors.cs create mode 100644 tests/ConsoleAppFramework.GeneratorTests/CSharpIncrementalSourceGeneratorVerifier.cs diff --git a/src/ConsoleAppFramework5/DiagnosticDescriptors.cs b/src/ConsoleAppFramework5/DiagnosticDescriptors.cs new file mode 100644 index 0000000..e1ec32b --- /dev/null +++ b/src/ConsoleAppFramework5/DiagnosticDescriptors.cs @@ -0,0 +1,21 @@ +using Microsoft.CodeAnalysis; + +namespace ConsoleAppFramework; + +internal static class DiagnosticDescriptors +{ + const string Category = "GenerateConsoleAppFramework"; + + public static readonly DiagnosticDescriptor RequireArgsAndMethod = new( + id: "CAF001", + title: "ConsoleApp methods require string[] args and lambda/method.", + messageFormat: "ConsoleApp.Run/RunAsync requires string[] args and lambda/method in arguments.", + category: Category, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public static Diagnostic Create(this DiagnosticDescriptor diagnostic, Location location, params object?[]? messageArgs) + { + return Diagnostic.Create(diagnostic, location, messageArgs); + } +} diff --git a/src/ConsoleAppFramework5/Parser.cs b/src/ConsoleAppFramework5/Parser.cs index 2e5d6bd..16b4270 100644 --- a/src/ConsoleAppFramework5/Parser.cs +++ b/src/ConsoleAppFramework5/Parser.cs @@ -9,6 +9,12 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta public Command? ParseAndValidate() { var args = node.ArgumentList.Arguments; + if (args.Count is 0 or 1) + { + context.ReportDiagnostic(DiagnosticDescriptors.RequireArgsAndMethod.Create(node.GetLocation())); + return null; + } + if (args.Count == 2) // 0 = args, 1 = lambda { var lambda = args[1].Expression as ParenthesizedLambdaExpressionSyntax; diff --git a/tests/ConsoleAppFramework.GeneratorTests/CSharpIncrementalSourceGeneratorVerifier.cs b/tests/ConsoleAppFramework.GeneratorTests/CSharpIncrementalSourceGeneratorVerifier.cs new file mode 100644 index 0000000..7ea7958 --- /dev/null +++ b/tests/ConsoleAppFramework.GeneratorTests/CSharpIncrementalSourceGeneratorVerifier.cs @@ -0,0 +1,34 @@ +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis.Testing.Verifiers; +using Microsoft.CodeAnalysis.CSharp.Testing; + +namespace ConsoleAppFramework.GeneratorTests; + +// https://github.com/dotnet/roslyn/blob/main/docs/features/source-generators.cookbook.md#unit-testing-of-generators + +public static class CSharpIncrementalSourceGeneratorVerifier + where TSourceGenerator : IIncrementalGenerator, new() +{ + public class Test : SourceGeneratorTest + { + public CSharpCompilationOptions CompilationOptions { get; set; } = new(OutputKind.DynamicallyLinkedLibrary); + protected override CompilationOptions CreateCompilationOptions() => CompilationOptions; + public CSharpParseOptions ParseOptions { get; set; } = new(languageVersion: LanguageVersion.CSharp12, kind: SourceCodeKind.Regular, documentationMode: DocumentationMode.Parse); + protected override ParseOptions CreateParseOptions() => ParseOptions; + public AnalyzerConfigOptionsProvider? AnalyzerConfigOptionsProvider { get; set; } + + protected override string DefaultFileExt => "cs"; + public override string Language => LanguageNames.CSharp; + protected override IEnumerable GetSourceGenerators() => new[] { new TSourceGenerator().AsSourceGenerator() }; + protected override GeneratorDriver CreateGeneratorDriver(Project project, ImmutableArray sourceGenerators) + => CSharpGeneratorDriver.Create( + sourceGenerators, + project.AnalyzerOptions.AdditionalFiles, + (CSharpParseOptions)project.ParseOptions!, + AnalyzerConfigOptionsProvider ?? project.AnalyzerOptions.AnalyzerConfigOptionsProvider); + } +} diff --git a/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppFramework.GeneratorTests.csproj b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppFramework.GeneratorTests.csproj index 0094e66..0de8845 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppFramework.GeneratorTests.csproj +++ b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppFramework.GeneratorTests.csproj @@ -13,7 +13,7 @@ - + diff --git a/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs b/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs index 2a07b8b..ee1099c 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs @@ -1,11 +1,19 @@ -using System; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis; +using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using Microsoft; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Text; namespace ConsoleAppFramework.GeneratorTests; +using VerifyCS = CSharpIncrementalSourceGeneratorVerifier; + + public class DiagnosticsTest { static void Verify(int id, string code, bool allowMultipleError = false) @@ -22,5 +30,30 @@ static void Verify(int id, string code, bool allowMultipleError = false) } } + [Fact] + public async Task Foo() + { + var code = """ +using System; +using ConsoleAppFramework; +using System.ComponentModel.DataAnnotations; + +ConsoleApp.Run(args, ([Range(1, 10)]int x, [Range(100, 200)]int y) => { Console.Write((x + y)); }); +"""; + var generated = "expected generated code"; + await new VerifyCS.Test + { + TestState = + { + Sources = { code }, + GeneratedSources = + { + (typeof(ConsoleAppGenerator), "GeneratedFileName", SourceText.From(generated, Encoding.UTF8, SourceHashAlgorithm.Sha256)), + }, + }, + + }.RunAsync(); + } } + diff --git a/tests/ConsoleAppFramework.GeneratorTests/RunTest.cs b/tests/ConsoleAppFramework.GeneratorTests/RunTest.cs index d5cd3bb..5a685a1 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/RunTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/RunTest.cs @@ -2,6 +2,7 @@ using Microsoft.CodeAnalysis; using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -9,22 +10,8 @@ namespace ConsoleAppFramework.GeneratorTests; -public class Test // (ITestOutputHelper output) +public class Test(ITestOutputHelper output) { - static void Verify(int id, string code, bool allowMultipleError = false) - { - var diagnostics = CSharpGeneratorRunner.RunAndGetErrorDiagnostics(code); - if (!allowMultipleError) - { - diagnostics.Length.Should().Be(1); - diagnostics[0].Id.Should().Be("CAF" + id.ToString("000")); - } - else - { - diagnostics.Select(x => x.Id).Should().Contain("CAF" + id.ToString("000")); - } - } - static string[] ToArgs(string args) { return args.Split(' '); @@ -37,12 +24,56 @@ public void SyncRun() using System; using ConsoleAppFramework; -ConsoleApp.Run(args, (int x, int y) => { Console.WriteLine((x + y)); }); +ConsoleApp.Run(args, (int x, int y) => { Console.Write((x + y)); }); """, ToArgs("--x 10 --y 20")); - result.Should().Be(""" -30 + result.Should().Be("30"); + } + + [Fact] + public void ValidateOne() + { + var result = CSharpGeneratorRunner.CompileAndExecute(""" +using System; +using ConsoleAppFramework; +using System.ComponentModel.DataAnnotations; + +ConsoleApp.Run(args, ([Range(1, 10)]int x, [Range(100, 200)]int y) => { Console.Write((x + y)); }); +""", ToArgs("--x 100 --y 140")); + + var expected = """ +The field x must be between 1 and 10. + + +"""; + + result.Should().Be(expected); + + Environment.ExitCode.Should().Be(1); + Environment.ExitCode = 0; + } + + [Fact] + public void ValidateTwo() + { + var result = CSharpGeneratorRunner.CompileAndExecute(""" +using System; +using ConsoleAppFramework; +using System.ComponentModel.DataAnnotations; + +ConsoleApp.Run(args, ([Range(1, 10)]int x, [Range(100, 200)]int y) => { Console.Write((x + y)); }); +""", ToArgs("--x 100 --y 240")); + + var expected = """ +The field x must be between 1 and 10. +The field y must be between 100 and 200. + + +"""; + + result.Should().Be(expected); -"""); + Environment.ExitCode.Should().Be(1); + Environment.ExitCode = 0; } } \ No newline at end of file From 65b3fcab26ab79f99ae2f8dfaa03a89e584a5fc0 Mon Sep 17 00:00:00 2001 From: neuecc Date: Sat, 18 May 2024 20:11:38 +0900 Subject: [PATCH 11/54] foundation 2 --- sandbox/GeneratorSandbox/Program.cs | 7 +-- .../ConsoleAppGenerator.cs | 10 +-- .../CSharpGeneratorRunner.cs | 12 ++-- ...SharpIncrementalSourceGeneratorVerifier.cs | 34 ---------- .../ConsoleAppFramework.GeneratorTests.csproj | 2 - .../DiagnosticsTest.cs | 63 ++++++++----------- 6 files changed, 41 insertions(+), 87 deletions(-) delete mode 100644 tests/ConsoleAppFramework.GeneratorTests/CSharpIncrementalSourceGeneratorVerifier.cs diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index 9322a36..a41a3b4 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -54,16 +54,11 @@ //ConsoleApp.Run(args, ([Vector3Parser] Vector3 x) => //{Quaternion// //}); - ConsoleApp.Run(args, ([Range(1, 10)] int x, [StringLength(10)] string y) => + ConsoleApp.Run(args, (int x, int y) => { - - Console.WriteLine("OK"); - - }); - } // description diff --git a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs index 4cec8a3..84fb508 100644 --- a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs @@ -37,9 +37,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) context.RegisterSourceOutput(source, EmitConsoleAppRun); } - static void EmitConsoleAppTemplateSource(IncrementalGeneratorPostInitializationContext context) - { - context.AddSource("ConsoleApp.cs", """ + public const string ConsoleAppBaseCode = """ // #nullable enable namespace ConsoleAppFramework; @@ -288,7 +286,11 @@ public void Dispose() } } } -"""); +"""; + + static void EmitConsoleAppTemplateSource(IncrementalGeneratorPostInitializationContext context) + { + context.AddSource("ConsoleApp.cs", ConsoleAppBaseCode); } static void EmitConsoleAppRun(SourceProductionContext sourceProductionContext, (InvocationExpressionSyntax, SemanticModel) generatorSyntaxContext) diff --git a/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs b/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs index fef6eff..b6fabf7 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs @@ -2,6 +2,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Immutable; using System.Runtime.CompilerServices; using System.Runtime.Loader; @@ -33,7 +34,7 @@ public static void InitializeCompilation() baseCompilation = compilation; } - public static Compilation RunGenerator(string source, string[]? preprocessorSymbols = null, AnalyzerConfigOptionsProvider? options = null) + public static (Compilation, ImmutableArray) RunGenerator(string source, string[]? preprocessorSymbols = null, AnalyzerConfigOptionsProvider? options = null) { if (preprocessorSymbols == null) { @@ -50,18 +51,18 @@ public static Compilation RunGenerator(string source, string[]? preprocessorSymb var compilation = baseCompilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(source, parseOptions)); driver.RunGeneratorsAndUpdateCompilation(compilation, out var newCompilation, out var diagnostics); - return newCompilation!; + return (newCompilation, diagnostics); } public static Diagnostic[] RunAndGetErrorDiagnostics(string source, string[]? preprocessorSymbols = null, AnalyzerConfigOptionsProvider? options = null) { - var compilation = RunGenerator(source, preprocessorSymbols, options); - return compilation.GetDiagnostics().Where(x => x.Severity == DiagnosticSeverity.Error).ToArray(); + var (compilation, diagnostics) = RunGenerator(source, preprocessorSymbols, options); + return diagnostics.Concat(compilation.GetDiagnostics()).Where(x => x.Severity == DiagnosticSeverity.Error).ToArray(); } public static string CompileAndExecute(string source, string[] args, string[]? preprocessorSymbols = null, AnalyzerConfigOptionsProvider? options = null) { - var compilation = RunGenerator(source, preprocessorSymbols, options); + var (compilation, diagnostics) = RunGenerator(source, preprocessorSymbols, options); using var ms = new MemoryStream(); var emitResult = compilation.Emit(ms); @@ -73,6 +74,7 @@ public static string CompileAndExecute(string source, string[] args, string[]? p ms.Position = 0; // capture stdout log + // modify global stdout so can't run in parallel unit-test var originalOut = Console.Out; try { diff --git a/tests/ConsoleAppFramework.GeneratorTests/CSharpIncrementalSourceGeneratorVerifier.cs b/tests/ConsoleAppFramework.GeneratorTests/CSharpIncrementalSourceGeneratorVerifier.cs deleted file mode 100644 index 7ea7958..0000000 --- a/tests/ConsoleAppFramework.GeneratorTests/CSharpIncrementalSourceGeneratorVerifier.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Testing; -using Microsoft.CodeAnalysis; -using System.Collections.Immutable; -using Microsoft.CodeAnalysis.Testing.Verifiers; -using Microsoft.CodeAnalysis.CSharp.Testing; - -namespace ConsoleAppFramework.GeneratorTests; - -// https://github.com/dotnet/roslyn/blob/main/docs/features/source-generators.cookbook.md#unit-testing-of-generators - -public static class CSharpIncrementalSourceGeneratorVerifier - where TSourceGenerator : IIncrementalGenerator, new() -{ - public class Test : SourceGeneratorTest - { - public CSharpCompilationOptions CompilationOptions { get; set; } = new(OutputKind.DynamicallyLinkedLibrary); - protected override CompilationOptions CreateCompilationOptions() => CompilationOptions; - public CSharpParseOptions ParseOptions { get; set; } = new(languageVersion: LanguageVersion.CSharp12, kind: SourceCodeKind.Regular, documentationMode: DocumentationMode.Parse); - protected override ParseOptions CreateParseOptions() => ParseOptions; - public AnalyzerConfigOptionsProvider? AnalyzerConfigOptionsProvider { get; set; } - - protected override string DefaultFileExt => "cs"; - public override string Language => LanguageNames.CSharp; - protected override IEnumerable GetSourceGenerators() => new[] { new TSourceGenerator().AsSourceGenerator() }; - protected override GeneratorDriver CreateGeneratorDriver(Project project, ImmutableArray sourceGenerators) - => CSharpGeneratorDriver.Create( - sourceGenerators, - project.AnalyzerOptions.AdditionalFiles, - (CSharpParseOptions)project.ParseOptions!, - AnalyzerConfigOptionsProvider ?? project.AnalyzerOptions.AnalyzerConfigOptionsProvider); - } -} diff --git a/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppFramework.GeneratorTests.csproj b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppFramework.GeneratorTests.csproj index 0de8845..961f197 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppFramework.GeneratorTests.csproj +++ b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppFramework.GeneratorTests.csproj @@ -11,9 +11,7 @@ - - diff --git a/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs b/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs index ee1099c..3d6b254 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs @@ -1,58 +1,49 @@ -using Microsoft.CodeAnalysis.Testing; -using Microsoft.CodeAnalysis; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft; -using Microsoft.CodeAnalysis.CSharp.Testing; -using Microsoft.CodeAnalysis.Text; +using Microsoft.CodeAnalysis; namespace ConsoleAppFramework.GeneratorTests; -using VerifyCS = CSharpIncrementalSourceGeneratorVerifier; - public class DiagnosticsTest { - static void Verify(int id, string code, bool allowMultipleError = false) + static string Verify(int id, string code) { var diagnostics = CSharpGeneratorRunner.RunAndGetErrorDiagnostics(code); - if (!allowMultipleError) - { - diagnostics.Length.Should().Be(1); - diagnostics[0].Id.Should().Be("CAF" + id.ToString("000")); - } - else + diagnostics.Length.Should().Be(1); + diagnostics[0].Id.Should().Be("CAF" + id.ToString("000")); + return GetLocationText(diagnostics[0]); + } + + static (string, string)[] Verify(string code) + { + var diagnostics = CSharpGeneratorRunner.RunAndGetErrorDiagnostics(code); + return diagnostics.Select(x => (x.Id, GetLocationText(x))).ToArray(); + } + + static string GetLocationText(Diagnostic diagnostic) + { + var location = diagnostic.Location; + var textSpan = location.SourceSpan; + var sourceTree = location.SourceTree; + if (sourceTree == null) { - diagnostics.Select(x => x.Id).Should().Contain("CAF" + id.ToString("000")); + return ""; } + + var text = sourceTree.GetText().GetSubText(textSpan).ToString(); + return text; } [Fact] - public async Task Foo() + public void ArgumentCount() { var code = """ using System; using ConsoleAppFramework; -using System.ComponentModel.DataAnnotations; -ConsoleApp.Run(args, ([Range(1, 10)]int x, [Range(100, 200)]int y) => { Console.Write((x + y)); }); +ConsoleApp.Run(args); """; - var generated = "expected generated code"; - await new VerifyCS.Test - { - TestState = - { - Sources = { code }, - GeneratedSources = - { - (typeof(ConsoleAppGenerator), "GeneratedFileName", SourceText.From(generated, Encoding.UTF8, SourceHashAlgorithm.Sha256)), - }, - }, - - }.RunAsync(); + + Verify(1, code).Should().Be("ConsoleApp.Run(args)"); } } From fcf94ecf8d5ebe9dcbb93e51824499db243f858e Mon Sep 17 00:00:00 2001 From: neuecc Date: Sat, 18 May 2024 23:17:56 +0900 Subject: [PATCH 12/54] validation 2 --- .../GeneratorSandbox/GeneratorSandbox.csproj | 3 +- sandbox/GeneratorSandbox/Program.cs | 36 +------- .../DiagnosticDescriptors.cs | 38 ++++++-- src/ConsoleAppFramework5/Parser.cs | 28 +++--- .../CSharpGeneratorRunner.cs | 91 +++++++++++++++++-- .../DiagnosticsTest.cs | 84 ++++++++++------- .../RunTest.cs | 2 +- 7 files changed, 185 insertions(+), 97 deletions(-) diff --git a/sandbox/GeneratorSandbox/GeneratorSandbox.csproj b/sandbox/GeneratorSandbox/GeneratorSandbox.csproj index 60f5cf1..3287be0 100644 --- a/sandbox/GeneratorSandbox/GeneratorSandbox.csproj +++ b/sandbox/GeneratorSandbox/GeneratorSandbox.csproj @@ -6,10 +6,11 @@ enable enable true + 1701;1702;CS8321 - + diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index a41a3b4..b1ed453 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -20,7 +20,7 @@ args = ["--x", "18", "--y", "aiueokakikukeko"]; // test. - +ConsoleApp.Run(args, Run2); void Run2(int x, int y) { }; // var s = "foo"; // s.AsSpan().Split(',',). @@ -40,42 +40,10 @@ -unsafe -{ - ConsoleApp.LogError = Console.Error.WriteLine; - - // GetValidationResult - - - - - - - //ConsoleApp.Run(args, ([Vector3Parser] Vector3 x) => - //{Quaternion// - //}); - ConsoleApp.Run(args, (int x, int y) => - { - }); - - -} - -// description -// - -var sc = new ServiceCollection(); -sc.AddSingleton(); -var provider = sc.BuildServiceProvider(); -ConsoleApp.ServiceProvider = provider; - - - - - static async Task RunRun(int? x = null, string? y = null) { + await Task.Yield(); Console.WriteLine("Hello World!" + x + y); return 0; } diff --git a/src/ConsoleAppFramework5/DiagnosticDescriptors.cs b/src/ConsoleAppFramework5/DiagnosticDescriptors.cs index e1ec32b..092c86e 100644 --- a/src/ConsoleAppFramework5/DiagnosticDescriptors.cs +++ b/src/ConsoleAppFramework5/DiagnosticDescriptors.cs @@ -6,16 +6,36 @@ internal static class DiagnosticDescriptors { const string Category = "GenerateConsoleAppFramework"; - public static readonly DiagnosticDescriptor RequireArgsAndMethod = new( - id: "CAF001", - title: "ConsoleApp methods require string[] args and lambda/method.", - messageFormat: "ConsoleApp.Run/RunAsync requires string[] args and lambda/method in arguments.", - category: Category, - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true); + public static void ReportDiagnostic(this SourceProductionContext context, DiagnosticDescriptor diagnosticDescriptor, Location location, params object?[]? messageArgs) + { + var diagnostic = Diagnostic.Create(diagnosticDescriptor, location, messageArgs); + context.ReportDiagnostic(diagnostic); + } - public static Diagnostic Create(this DiagnosticDescriptor diagnostic, Location location, params object?[]? messageArgs) + public static DiagnosticDescriptor Create(int id, string title, string messageFormat) { - return Diagnostic.Create(diagnostic, location, messageArgs); + return new DiagnosticDescriptor( + id: "CAF" + id.ToString("000"), + title: title, + messageFormat: messageFormat, + category: Category, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); } + + public static readonly DiagnosticDescriptor RequireArgsOrMethod = Create( + 1, + "ConsoleApp methods require string[] args and lambda/method.", + "ConsoleApp.Run/RunAsync requires string[] args and lambda/method in arguments."); + + public static readonly DiagnosticDescriptor ReturnTypeLambda = Create( + 2, + "Run lambda expressions return type must be void or int or async Task or async Task.", + "Run lambda expressions return type must be void or int or async Task or async Task but returned '{0}'."); + + public static readonly DiagnosticDescriptor ReturnTypeMethod = Create( + 3, + "Run referenced method return type must be void or int or async Task or async Task.", + "Run referenced method return type must be void or int or async Task or async Task but returned '{0}'."); + } diff --git a/src/ConsoleAppFramework5/Parser.cs b/src/ConsoleAppFramework5/Parser.cs index 16b4270..a2e55a7 100644 --- a/src/ConsoleAppFramework5/Parser.cs +++ b/src/ConsoleAppFramework5/Parser.cs @@ -9,12 +9,6 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta public Command? ParseAndValidate() { var args = node.ArgumentList.Arguments; - if (args.Count is 0 or 1) - { - context.ReportDiagnostic(DiagnosticDescriptors.RequireArgsAndMethod.Create(node.GetLocation())); - return null; - } - if (args.Count == 2) // 0 = args, 1 = lambda { var lambda = args[1].Expression as ParenthesizedLambdaExpressionSyntax; @@ -38,8 +32,6 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta return ParseFromMethodSymbol(methodSymbol, addressOf: false); } } - - return null; } else { @@ -47,6 +39,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta } } + context.ReportDiagnostic(DiagnosticDescriptors.RequireArgsOrMethod, node.GetLocation()); return null; } @@ -70,16 +63,22 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta } else { - // others, invalid. - // TODO: validation invalid. + context.ReportDiagnostic(DiagnosticDescriptors.ReturnTypeLambda, lambda.ReturnType!.GetLocation(), lambda.ReturnType); + return null; } } else { + if (!(lambda.ReturnType?.ToString() is "Task" or "Task")) + { + context.ReportDiagnostic(DiagnosticDescriptors.ReturnTypeLambda, lambda.ReturnType!.GetLocation(), lambda.ReturnType); + return null; + } + var firstType = (lambda.ReturnType as GenericNameSyntax)?.TypeArgumentList.Arguments.FirstOrDefault(); if (firstType == null) { - isVoid = true; // strictly, should check ret-type is Task... + isVoid = true; } else if ((firstType as PredefinedTypeSyntax)?.Keyword.IsKind(SyntaxKind.IntKeyword) ?? false) { @@ -87,7 +86,8 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta } else { - // TODO: validation invalid + context.ReportDiagnostic(DiagnosticDescriptors.ReturnTypeLambda, lambda.ReturnType!.GetLocation(), lambda.ReturnType); + return null; } } } @@ -257,13 +257,13 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta } else { - // TODO: invalid return + context.ReportDiagnostic(DiagnosticDescriptors.ReturnTypeMethod, node.ArgumentList.Arguments[1].GetLocation(), methodSymbol.ReturnType); return null; } } else { - // TODO: invalid return type + context.ReportDiagnostic(DiagnosticDescriptors.ReturnTypeMethod, node.ArgumentList.Arguments[1].GetLocation(), methodSymbol.ReturnType); return null; } diff --git a/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs b/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs index b6fabf7..a84e225 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs @@ -5,6 +5,7 @@ using System.Collections.Immutable; using System.Runtime.CompilerServices; using System.Runtime.Loader; +using Xunit.Abstractions; public static class CSharpGeneratorRunner { @@ -27,8 +28,15 @@ public static void InitializeCompilation() .Select(x => MetadataReference.CreateFromFile(x)) .ToArray(); + var globalUsings = """ +global using System; +global using ConsoleAppFramework; +global using System.Threading.Tasks; +"""; + var compilation = CSharpCompilation.Create("generatortest", references: references, + syntaxTrees: [CSharpSyntaxTree.ParseText(globalUsings)], options: new CSharpCompilationOptions(OutputKind.ConsoleApplication)); // .exe baseCompilation = compilation; @@ -54,12 +62,6 @@ public static (Compilation, ImmutableArray) RunGenerator(string sour return (newCompilation, diagnostics); } - public static Diagnostic[] RunAndGetErrorDiagnostics(string source, string[]? preprocessorSymbols = null, AnalyzerConfigOptionsProvider? options = null) - { - var (compilation, diagnostics) = RunGenerator(source, preprocessorSymbols, options); - return diagnostics.Concat(compilation.GetDiagnostics()).Where(x => x.Severity == DiagnosticSeverity.Error).ToArray(); - } - public static string CompileAndExecute(string source, string[] args, string[]? preprocessorSymbols = null, AnalyzerConfigOptionsProvider? options = null) { var (compilation, diagnostics) = RunGenerator(source, preprocessorSymbols, options); @@ -94,4 +96,81 @@ public static string CompileAndExecute(string source, string[] args, string[]? p Console.SetOut(originalOut); } } +} + +public class VerifyHelper(ITestOutputHelper output, string idPrefix) +{ + public void Ok(string code) + { + var (compilation, diagnostics) = CSharpGeneratorRunner.RunGenerator(code); + foreach (var item in diagnostics) + { + output.WriteLine(item.ToString()); + } + OutputGeneratedCode(compilation); + + diagnostics.Length.Should().Be(0); + } + + public string Verify(int id, string code) + { + var (compilation, diagnostics) = CSharpGeneratorRunner.RunGenerator(code); + foreach (var item in diagnostics) + { + output.WriteLine(item.ToString()); + } + OutputGeneratedCode(compilation); + + diagnostics.Length.Should().Be(1); + diagnostics[0].Id.Should().Be(idPrefix + id.ToString("000")); + return GetLocationText(diagnostics[0]); + } + + public void Verify(int id, string code, string diagnosticsCodeSpan, [CallerArgumentExpression("code")] string? codeExpr = null) + { + output.WriteLine(codeExpr); + + var (compilation, diagnostics) = CSharpGeneratorRunner.RunGenerator(code); + foreach (var item in diagnostics) + { + output.WriteLine(item.ToString()); + } + OutputGeneratedCode(compilation); + + diagnostics.Length.Should().Be(1); + diagnostics[0].Id.Should().Be(idPrefix + id.ToString("000")); + var text = GetLocationText(diagnostics[0]); + text.Should().Be(diagnosticsCodeSpan); + } + + public (string, string)[] Verify(string code) + { + var (compilation, diagnostics) = CSharpGeneratorRunner.RunGenerator(code); + OutputGeneratedCode(compilation); + return diagnostics.Select(x => (x.Id, GetLocationText(x))).ToArray(); + } + + string GetLocationText(Diagnostic diagnostic) + { + var location = diagnostic.Location; + var textSpan = location.SourceSpan; + var sourceTree = location.SourceTree; + if (sourceTree == null) + { + return ""; + } + + var text = sourceTree.GetText().GetSubText(textSpan).ToString(); + return text; + } + + void OutputGeneratedCode(Compilation compilation) + { + foreach (var syntaxTree in compilation.SyntaxTrees) + { + // only shows ConsoleApp.Run generated code + if (!syntaxTree.FilePath.Contains("ConsoleApp.Run.cs")) continue; + output.WriteLine(syntaxTree.ToString()); + } + } } \ No newline at end of file diff --git a/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs b/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs index 3d6b254..2b4d03f 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs @@ -1,50 +1,70 @@ -using Microsoft.CodeAnalysis; +using Xunit.Abstractions; namespace ConsoleAppFramework.GeneratorTests; - -public class DiagnosticsTest +public class DiagnosticsTest(ITestOutputHelper output) { - static string Verify(int id, string code) + VerifyHelper verifier = new VerifyHelper(output, "CAF"); + + [Fact] + public void ArgumentCount() { - var diagnostics = CSharpGeneratorRunner.RunAndGetErrorDiagnostics(code); - diagnostics.Length.Should().Be(1); - diagnostics[0].Id.Should().Be("CAF" + id.ToString("000")); - return GetLocationText(diagnostics[0]); + verifier.Verify(1, "ConsoleApp.Run(args);", "ConsoleApp.Run(args)"); + verifier.Verify(1, "ConsoleApp.Run();", "ConsoleApp.Run()"); + verifier.Verify(1, "ConsoleApp.Run(args, (int x, int y) => { }, 1000);", "ConsoleApp.Run(args, (int x, int y) => { }, 1000)"); } - static (string, string)[] Verify(string code) + [Fact] + public void InvalidReturnTypeFromLambda() { - var diagnostics = CSharpGeneratorRunner.RunAndGetErrorDiagnostics(code); - return diagnostics.Select(x => (x.Id, GetLocationText(x))).ToArray(); + verifier.Verify(2, "ConsoleApp.Run(args, string (int x, int y) => { return \"foo\"; })", "string"); + verifier.Verify(2, "ConsoleApp.Run(args, int? (int x, int y) => { return -1; })", "int?"); + verifier.Verify(2, "ConsoleApp.Run(args, Task (int x, int y) => { return Task.CompletedTask; })", "Task"); + verifier.Verify(2, "ConsoleApp.Run(args, Task (int x, int y) => { return Task.FromResult(0); })", "Task"); + verifier.Verify(2, "ConsoleApp.Run(args, async Task (int x, int y) => { return \"foo\"; })", "Task"); + verifier.Verify(2, "ConsoleApp.Run(args, async ValueTask (int x, int y) => { })", "ValueTask"); + verifier.Verify(2, "ConsoleApp.Run(args, async ValueTask (int x, int y) => { return -1; })", "ValueTask"); + verifier.Ok("ConsoleApp.Run(args, (int x, int y) => { })"); + verifier.Ok("ConsoleApp.Run(args, void (int x, int y) => { })"); + verifier.Ok("ConsoleApp.Run(args, int (int x, int y) => { })"); + verifier.Ok("ConsoleApp.Run(args, async Task (int x, int y) => { })"); + verifier.Ok("ConsoleApp.Run(args, async Task (int x, int y) => { })"); } - static string GetLocationText(Diagnostic diagnostic) + [Fact] + public void InvalidReturnTypeFromMethodReference() { - var location = diagnostic.Location; - var textSpan = location.SourceSpan; - var sourceTree = location.SourceTree; - if (sourceTree == null) - { - return ""; - } - - var text = sourceTree.GetText().GetSubText(textSpan).ToString(); - return text; + verifier.Verify(3, "ConsoleApp.Run(args, Invoke); float Invoke(int x, int y) => 0.3f;", "Invoke"); + verifier.Verify(3, "ConsoleApp.Run(args, InvokeAsync); async Task InvokeAsync(int x, int y) => 0.3f;", "InvokeAsync"); + verifier.Ok("ConsoleApp.Run(args, Run); void Run(int x, int y) { };"); + verifier.Ok("ConsoleApp.Run(args, Run); static void Run(int x, int y) { };"); + verifier.Ok("ConsoleApp.Run(args, Run); int Run(int x, int y) => -1;"); + verifier.Ok("ConsoleApp.Run(args, Run); async Task Run(int x, int y) { };"); + verifier.Ok("ConsoleApp.Run(args, Run); async Task Run(int x, int y) => -1;"); } [Fact] - public void ArgumentCount() + public void RunAsyncValidation() { - var code = """ -using System; -using ConsoleAppFramework; - -ConsoleApp.Run(args); -"""; + verifier.Verify(2, "ConsoleApp.RunAsync(args, string (int x, int y) => { return \"foo\"; })", "string"); + verifier.Verify(2, "ConsoleApp.RunAsync(args, int? (int x, int y) => { return -1; })", "int?"); + verifier.Verify(2, "ConsoleApp.RunAsync(args, Task (int x, int y) => { return Task.CompletedTask; })", "Task"); + verifier.Verify(2, "ConsoleApp.RunAsync(args, Task (int x, int y) => { return Task.FromResult(0); })", "Task"); + verifier.Verify(2, "ConsoleApp.RunAsync(args, async Task (int x, int y) => { return \"foo\"; })", "Task"); + verifier.Verify(2, "ConsoleApp.RunAsync(args, async ValueTask (int x, int y) => { })", "ValueTask"); + verifier.Verify(2, "ConsoleApp.RunAsync(args, async ValueTask (int x, int y) => { return -1; })", "ValueTask"); + verifier.Ok("ConsoleApp.RunAsync(args, (int x, int y) => { })"); + verifier.Ok("ConsoleApp.RunAsync(args, void (int x, int y) => { })"); + verifier.Ok("ConsoleApp.RunAsync(args, int (int x, int y) => { })"); + verifier.Ok("ConsoleApp.RunAsync(args, async Task (int x, int y) => { })"); + verifier.Ok("ConsoleApp.RunAsync(args, async Task (int x, int y) => { })"); - Verify(1, code).Should().Be("ConsoleApp.Run(args)"); + verifier.Verify(3, "ConsoleApp.RunAsync(args, Invoke); float Invoke(int x, int y) => 0.3f;", "Invoke"); + verifier.Verify(3, "ConsoleApp.RunAsync(args, InvokeAsync); async Task InvokeAsync(int x, int y) => 0.3f;", "InvokeAsync"); + verifier.Ok("ConsoleApp.RunAsync(args, Run); void Run(int x, int y) { };"); + verifier.Ok("ConsoleApp.RunAsync(args, Run); static void Run(int x, int y) { };"); + verifier.Ok("ConsoleApp.RunAsync(args, Run); int Run(int x, int y) => -1;"); + verifier.Ok("ConsoleApp.RunAsync(args, Run); async Task Run(int x, int y) { };"); + verifier.Ok("ConsoleApp.RunAsync(args, Run); async Task Run(int x, int y) => -1;"); } - } - diff --git a/tests/ConsoleAppFramework.GeneratorTests/RunTest.cs b/tests/ConsoleAppFramework.GeneratorTests/RunTest.cs index 5a685a1..d34e40c 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/RunTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/RunTest.cs @@ -10,7 +10,7 @@ namespace ConsoleAppFramework.GeneratorTests; -public class Test(ITestOutputHelper output) +public class Test // (ITestOutputHelper output) { static string[] ToArgs(string args) { From 616cdecf28ff0a5196e7348aee5540438152e491 Mon Sep 17 00:00:00 2001 From: neuecc Date: Sun, 19 May 2024 02:34:00 +0900 Subject: [PATCH 13/54] validation ok --- sandbox/GeneratorSandbox/Program.cs | 4 +- src/ConsoleAppFramework5/Command.cs | 3 +- .../DiagnosticDescriptors.cs | 24 ++++++--- src/ConsoleAppFramework5/Parser.cs | 50 +++++++++++++++++-- .../CSharpGeneratorRunner.cs | 25 +++------- .../DiagnosticsTest.cs | 19 +++++++ 6 files changed, 97 insertions(+), 28 deletions(-) diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index b1ed453..b590eb4 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -20,7 +20,9 @@ args = ["--x", "18", "--y", "aiueokakikukeko"]; // test. -ConsoleApp.Run(args, Run2); void Run2(int x, int y) { }; +// ConsoleApp.Run(args, Run2); void Run2(int x, int yzzzz) { }; + +unsafe { ConsoleApp.Run(args, &Run2); static void Run2(int x, [System.ComponentModel.DataAnnotations.Range(0, 10)]int y) { }; } // var s = "foo"; // s.AsSpan().Split(',',). diff --git a/src/ConsoleAppFramework5/Command.cs b/src/ConsoleAppFramework5/Command.cs index 53a75f6..d38e144 100644 --- a/src/ConsoleAppFramework5/Command.cs +++ b/src/ConsoleAppFramework5/Command.cs @@ -124,6 +124,7 @@ public string BuildDelegateType(string delegateName) public record class CommandParameter { public required ITypeSymbol Type { get; init; } + public required Location Location { get; init; } public required bool IsNullableReference { get; init; } public required string Name { get; init; } public required bool HasDefaultValue { get; init; } @@ -132,7 +133,7 @@ public record class CommandParameter public required bool IsFromServices { get; init; } public required bool IsCancellationToken { get; init; } public bool IsParsable => !(IsFromServices || IsCancellationToken); - + // 追加!コンパイルエラーありがたい! public required bool HasValidation { get; init; } diff --git a/src/ConsoleAppFramework5/DiagnosticDescriptors.cs b/src/ConsoleAppFramework5/DiagnosticDescriptors.cs index 092c86e..1ceb8bb 100644 --- a/src/ConsoleAppFramework5/DiagnosticDescriptors.cs +++ b/src/ConsoleAppFramework5/DiagnosticDescriptors.cs @@ -12,6 +12,11 @@ public static void ReportDiagnostic(this SourceProductionContext context, Diagno context.ReportDiagnostic(diagnostic); } + public static DiagnosticDescriptor Create(int id, string message) + { + return Create(id, message, message); + } + public static DiagnosticDescriptor Create(int id, string title, string messageFormat) { return new DiagnosticDescriptor( @@ -25,17 +30,24 @@ public static DiagnosticDescriptor Create(int id, string title, string messageFo public static readonly DiagnosticDescriptor RequireArgsOrMethod = Create( 1, - "ConsoleApp methods require string[] args and lambda/method.", "ConsoleApp.Run/RunAsync requires string[] args and lambda/method in arguments."); public static readonly DiagnosticDescriptor ReturnTypeLambda = Create( 2, "Run lambda expressions return type must be void or int or async Task or async Task.", - "Run lambda expressions return type must be void or int or async Task or async Task but returned '{0}'."); - + "Run lambda expressions return type must be void or int or async Task or async Task but returned '{0}'."); + public static readonly DiagnosticDescriptor ReturnTypeMethod = Create( - 3, - "Run referenced method return type must be void or int or async Task or async Task.", - "Run referenced method return type must be void or int or async Task or async Task but returned '{0}'."); + 3, + "Run referenced method return type must be void or int or async Task or async Task.", + "Run referenced method return type must be void or int or async Task or async Task but returned '{0}'."); + + public static readonly DiagnosticDescriptor SequentialArgument = Create( + 4, + "All Argument parameters must be sequential from first."); + + public static readonly DiagnosticDescriptor FunctionPointerCanNotHaveValidation = Create( + 5, + "Function pointer can not have validation."); } diff --git a/src/ConsoleAppFramework5/Parser.cs b/src/ConsoleAppFramework5/Parser.cs index a2e55a7..7bc43f8 100644 --- a/src/ConsoleAppFramework5/Parser.cs +++ b/src/ConsoleAppFramework5/Parser.cs @@ -1,6 +1,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Reflection.Metadata; namespace ConsoleAppFramework; @@ -21,7 +22,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta var methodSymbols = model.GetMemberGroup(operand); if (methodSymbols.Length > 0 && methodSymbols[0] is IMethodSymbol methodSymbol) { - return ParseFromMethodSymbol(methodSymbol, addressOf: true); + return ValidateCommand(ParseFromMethodSymbol(methodSymbol, addressOf: true)); } } else @@ -29,13 +30,13 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta var methodSymbols = model.GetMemberGroup(args[1].Expression); if (methodSymbols.Length > 0 && methodSymbols[0] is IMethodSymbol methodSymbol) { - return ParseFromMethodSymbol(methodSymbol, addressOf: false); + return ValidateCommand(ParseFromMethodSymbol(methodSymbol, addressOf: false)); } } } else { - return ParseFromLambda(lambda); + return ValidateCommand(ParseFromLambda(lambda)); } } @@ -192,6 +193,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta Name = x.Identifier.Text, IsNullableReference = isNullableReference, Type = type.Type!, + Location = x.GetLocation(), HasDefaultValue = hasDefault, DefaultValue = defaultValue, CustomParserType = customParserType, @@ -303,6 +305,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta { Name = x.Name, IsNullableReference = isNullableReference, + Location = x.DeclaringSyntaxReferences[0].GetSyntax().GetLocation(), Type = x.Type, HasDefaultValue = x.HasExplicitDefaultValue, DefaultValue = x.HasExplicitDefaultValue ? x.ExplicitDefaultValue : null, @@ -331,6 +334,47 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta return cmd; } + Command? ValidateCommand(Command? command) + { + if (command == null) return null; + var hasDiagnostic = false; + + // Sequential Argument + var existsNotArgument = false; + foreach (var parameter in command.Parameters) + { + if (!parameter.IsParsable) continue; + + if (!parameter.IsArgument) + { + existsNotArgument = true; + } + + if (parameter.IsArgument && existsNotArgument) + { + context.ReportDiagnostic(DiagnosticDescriptors.SequentialArgument, parameter.Location); + hasDiagnostic = true; + } + } + + // FunctionPointer can not use validation + if (command.MethodKind == MethodKind.FunctionPointer) + { + foreach (var p in command.Parameters) + { + if (p.HasValidation) + { + context.ReportDiagnostic(DiagnosticDescriptors.FunctionPointerCanNotHaveValidation, p.Location); + hasDiagnostic = true; + } + } + } + + if (hasDiagnostic) return null; + + return command; + } + void ParseParameterDescription(string originalDescription, out string[] aliases, out string description) { // Example: diff --git a/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs b/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs index a84e225..8cd9040 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs @@ -30,8 +30,9 @@ public static void InitializeCompilation() var globalUsings = """ global using System; -global using ConsoleAppFramework; global using System.Threading.Tasks; +global using System.ComponentModel.DataAnnotations; +global using ConsoleAppFramework; """; var compilation = CSharpCompilation.Create("generatortest", @@ -100,20 +101,10 @@ public static string CompileAndExecute(string source, string[] args, string[]? p public class VerifyHelper(ITestOutputHelper output, string idPrefix) { - public void Ok(string code) + public void Ok(string code, [CallerArgumentExpression("code")] string? codeExpr = null) { - var (compilation, diagnostics) = CSharpGeneratorRunner.RunGenerator(code); - foreach (var item in diagnostics) - { - output.WriteLine(item.ToString()); - } - OutputGeneratedCode(compilation); - - diagnostics.Length.Should().Be(0); - } + output.WriteLine(codeExpr); - public string Verify(int id, string code) - { var (compilation, diagnostics) = CSharpGeneratorRunner.RunGenerator(code); foreach (var item in diagnostics) { @@ -121,9 +112,7 @@ public string Verify(int id, string code) } OutputGeneratedCode(compilation); - diagnostics.Length.Should().Be(1); - diagnostics[0].Id.Should().Be(idPrefix + id.ToString("000")); - return GetLocationText(diagnostics[0]); + diagnostics.Length.Should().Be(0); } public void Verify(int id, string code, string diagnosticsCodeSpan, [CallerArgumentExpression("code")] string? codeExpr = null) @@ -143,8 +132,10 @@ public void Verify(int id, string code, string diagnosticsCodeSpan, [CallerArgum text.Should().Be(diagnosticsCodeSpan); } - public (string, string)[] Verify(string code) + public (string, string)[] Verify(string code, [CallerArgumentExpression("code")] string? codeExpr = null) { + output.WriteLine(codeExpr); + var (compilation, diagnostics) = CSharpGeneratorRunner.RunGenerator(code); OutputGeneratedCode(compilation); return diagnostics.Select(x => (x.Id, GetLocationText(x))).ToArray(); diff --git a/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs b/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs index 2b4d03f..e351508 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs @@ -67,4 +67,23 @@ public void RunAsyncValidation() verifier.Ok("ConsoleApp.RunAsync(args, Run); async Task Run(int x, int y) { };"); verifier.Ok("ConsoleApp.RunAsync(args, Run); async Task Run(int x, int y) => -1;"); } + + [Fact] + public void Argument() + { + verifier.Verify(4, "ConsoleApp.Run(args, (int x, [Argument]int y) => { })", "[Argument]int y"); + verifier.Verify(4, "ConsoleApp.Run(args, ([Argument]int x, int y, [Argument]int z) => { })", "[Argument]int z"); + verifier.Verify(4, "ConsoleApp.Run(args, Run); void Run(int x, [Argument]int y) { };", "[Argument]int y"); + + verifier.Ok("ConsoleApp.Run(args, ([Argument]int x, [Argument]int y) => { })"); + verifier.Ok("ConsoleApp.Run(args, Run); void Run([Argument]int x, [Argument]int y) { };"); + } + + [Fact] + public void FunctionPointerValidation() + { + verifier.Verify(5, "unsafe { ConsoleApp.Run(args, &Run2); static void Run2([Range(1, 10)]int x, int y) { }; }", "[Range(1, 10)]int x"); + + verifier.Ok("unsafe { ConsoleApp.Run(args, &Run2); static void Run2(int x, int y) { }; }"); + } } From 8c10ec3d67c869a45655df125db4777e6cb9db1d Mon Sep 17 00:00:00 2001 From: neuecc Date: Mon, 20 May 2024 09:09:07 +0900 Subject: [PATCH 14/54] WIP --- sandbox/GeneratorSandbox/Program.cs | 143 +++++++++++++++++- src/ConsoleAppFramework5/Command.cs | 2 +- .../ConsoleAppGenerator.cs | 121 ++++++++++++++- .../DiagnosticDescriptors.cs | 9 +- src/ConsoleAppFramework5/Emitter.cs | 73 ++++++++- src/ConsoleAppFramework5/Parser.cs | 89 +++++++---- 6 files changed, 396 insertions(+), 41 deletions(-) diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index b590eb4..3e37e1d 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -22,7 +22,31 @@ // ConsoleApp.Run(args, Run2); void Run2(int x, int yzzzz) { }; -unsafe { ConsoleApp.Run(args, &Run2); static void Run2(int x, [System.ComponentModel.DataAnnotations.Range(0, 10)]int y) { }; } + +var builder = ConsoleApp.CreateBuilder(); + +var aa = "foo"; + +builder.Add("foo", (int x, int y) => +{ +}); + +builder.Add("foo", (int x, int y) => +{ +}); + + + + +builder.Run(args); + + +//builder.Add("foo", (int x, int y) => x + y); +//builder.Add("bar", RunRun); + + + +unsafe { ConsoleApp.Run(args, Run2); static void Run2(int x, [System.ComponentModel.DataAnnotations.Range(0, 10)] int y) { }; } // var s = "foo"; // s.AsSpan().Split(',',). @@ -39,10 +63,6 @@ // --x - - - - static async Task RunRun(int? x = null, string? y = null) { await Task.Yield(); @@ -59,6 +79,119 @@ static void Tests() } + +// constructor injection! + +public class MyClass +{ + Action command1; // emit field + + public void Foo(string commandName, Delegate command) + { + // switch and cast + switch (commandName) + { + case "foo": + command1 = (Action)command; + break; + default: + break; + } + } + + //public void Foo(FooBar action2) + //{ + //} + + void Test() + { + Foo("takoyaki", (int x, int y = 10) => { }); + } +} + + +public delegate void FooBar(int x, int y = 10); + +public partial struct ConsoleAppBuilderTest +{ + public void Add(string commandName, Delegate command) + { + AddCore(commandName, command); + } + + [Conditional("DEBUG")] + public void Add() { } + + [Conditional("DEBUG")] + public void Add(string commandName) { } + + 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); +} + +//partial struct ConsoleAppBuilderTest +//{ +// Action command1; + +// // root command => / +// // sub command => foo/bar/baz +// public void Add(string commandName, Action command) // generate Add methods +// { +// // multi +// switch (commandName) +// { +// case "foo": +// this.command1 = command; +// break; +// } +// } + +// // or RunAsync +// public partial void Run(string[] args) // generate body +// { +// // --help? + +// switch (args[0]) +// { +// case "foo": +// RunCommand1(args.AsSpan(1), command1); +// break; +// case "bar": + +// break; +// } +// } + +// public partial Task RunAsync(string[] args) => throw new NotImplementedException(); + +// // generate both invoke and invokeasync? detect which calls? +// // void Invoke +// static void RunCommand1(Span args, Action command) +// { +// // call generated... +// } +//} + + + public static class Command { /// diff --git a/src/ConsoleAppFramework5/Command.cs b/src/ConsoleAppFramework5/Command.cs index d38e144..570f7c3 100644 --- a/src/ConsoleAppFramework5/Command.cs +++ b/src/ConsoleAppFramework5/Command.cs @@ -14,8 +14,8 @@ public record class Command { public required bool IsAsync { get; init; } // Task or Task public required bool IsVoid { get; init; } // void or int - public required bool IsRootCommand { get; init; } public required string CommandName { get; init; } + public bool IsRootCommand => CommandName == ""; public required CommandParameter[] Parameters { get; init; } public required string Description { get; init; } public required MethodKind MethodKind { get; init; } diff --git a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs index 84fb508..1ff5711 100644 --- a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs @@ -1,6 +1,8 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Collections.Immutable; +using System.Reflection; namespace ConsoleAppFramework; @@ -11,7 +13,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context) { context.RegisterPostInitializationOutput(EmitConsoleAppTemplateSource); - var source = context.SyntaxProvider.CreateSyntaxProvider((node, ct) => + // ConsoleApp.Run + var runSource = context.SyntaxProvider.CreateSyntaxProvider((node, ct) => { if (node.IsKind(SyntaxKind.InvocationExpression)) { @@ -34,7 +37,39 @@ public void Initialize(IncrementalGeneratorInitializationContext context) return false; }, (context, ct) => ((InvocationExpressionSyntax)context.Node, context.SemanticModel)); - context.RegisterSourceOutput(source, EmitConsoleAppRun); + context.RegisterSourceOutput(runSource, EmitConsoleAppRun); + + // ConsoleAppBuilder + var builderSource = context.SyntaxProvider + .CreateSyntaxProvider((node, ct) => + { + if (node.IsKind(SyntaxKind.InvocationExpression)) + { + var invocationExpression = (node as InvocationExpressionSyntax); + if (invocationExpression == null) return false; + + var expr = invocationExpression.Expression as MemberAccessExpressionSyntax; + var methodName = expr?.Name.Identifier.Text; + if (methodName is "Add" or "Run" or "RunAsync") + { + return true; + } + + return false; + } + + return false; + }, (context, ct) => ( + (InvocationExpressionSyntax)context.Node, + ((context.Node as InvocationExpressionSyntax)!.Expression as MemberAccessExpressionSyntax)!.Name.Identifier.Text, + context.SemanticModel)) + .Where(x => + { + var model = x.SemanticModel.GetTypeInfo((x.Item1.Expression as MemberAccessExpressionSyntax)!.Expression); + return model.Type?.Name == "ConsoleAppBuilder"; + }); + + context.RegisterSourceOutput(builderSource.Collect(), EmitConsoleAppBuilder); } public const string ConsoleAppBaseCode = """ @@ -95,6 +130,8 @@ public static Task RunAsync(string[] args) return Task.CompletedTask; } + public static ConsoleAppBuilder CreateBuilder() + static void ThrowArgumentParseFailed(string argumentName, string value) { throw new ArgumentException($"Argument '{argumentName}' parse failed. value: {value}"); @@ -286,6 +323,41 @@ public void Dispose() } } } + +public partial struct ConsoleAppBuilder +{ + public void Add(string commandName, Delegate command) + { + AddCore(commandName, command); + } + + [Conditional("DEBUG")] + public void Add() { } + + [Conditional("DEBUG")] + public void Add(string commandName) { } + + 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 void EmitConsoleAppTemplateSource(IncrementalGeneratorPostInitializationContext context) @@ -307,10 +379,10 @@ static void EmitConsoleAppRun(SourceProductionContext sourceProductionContext, ( return; } - var emitter = new Emitter(command, wellKnownTypes); + var emitter = new Emitter(wellKnownTypes); var isRunAsync = ((node.Expression as MemberAccessExpressionSyntax)?.Name.Identifier.Text == "RunAsync"); - var code = emitter.EmitRun(isRunAsync); + var code = emitter.EmitRun(command, isRunAsync); sourceProductionContext.AddSource("ConsoleApp.Run.cs", $$""" // @@ -341,4 +413,45 @@ internal static partial class ConsoleApp } """); } + + static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContext, ImmutableArray<(InvocationExpressionSyntax Node, string Name, SemanticModel Model)> generatorSyntaxContexts) + { + if (generatorSyntaxContexts.Length == 0) return; + + var model = generatorSyntaxContexts[0].Model; + + var wellKnownTypes = new WellKnownTypes(model.Compilation); + + var group1 = generatorSyntaxContexts.ToLookup(x => x.Name); + + // TODO: validation command name duplicate + var commands = group1["Add"] + .Select(x => + { + // TODO: Add handling + var parser = new Parser(sourceProductionContext, x.Node, x.Model, wellKnownTypes); + + var command = parser.ParseAndValidateForCommand(); + + // command.Parameters + + return command; + }) + .ToArray(); + + // don't emit if exists failure + if (commands.Any(x => x == null)) + { + return; + } + + + var run = group1["Run"]; + var runAsync = group1["RunAsync"]; + + var emitter = new Emitter(wellKnownTypes); + + + emitter.EmitBuilder(commands!, false); // TODO: isRunAsync + } } \ No newline at end of file diff --git a/src/ConsoleAppFramework5/DiagnosticDescriptors.cs b/src/ConsoleAppFramework5/DiagnosticDescriptors.cs index 1ceb8bb..215e29d 100644 --- a/src/ConsoleAppFramework5/DiagnosticDescriptors.cs +++ b/src/ConsoleAppFramework5/DiagnosticDescriptors.cs @@ -28,7 +28,7 @@ public static DiagnosticDescriptor Create(int id, string title, string messageFo isEnabledByDefault: true); } - public static readonly DiagnosticDescriptor RequireArgsOrMethod = Create( + public static readonly DiagnosticDescriptor RequireArgsAndMethod = Create( 1, "ConsoleApp.Run/RunAsync requires string[] args and lambda/method in arguments."); @@ -50,4 +50,11 @@ public static DiagnosticDescriptor Create(int id, string title, string messageFo 5, "Function pointer can not have validation."); + public static readonly DiagnosticDescriptor RequireCommandAndMethod = Create( + 6, + "ConsoleAppBuilder.Add requires string command and lambda/method in arguments or use Add."); + + public static readonly DiagnosticDescriptor AddCommandMustBeStringLiteral = Create( + 6, + "ConsoleAppBuilder.Add string command must be string literal."); } diff --git a/src/ConsoleAppFramework5/Emitter.cs b/src/ConsoleAppFramework5/Emitter.cs index 8fcb334..6ff098b 100644 --- a/src/ConsoleAppFramework5/Emitter.cs +++ b/src/ConsoleAppFramework5/Emitter.cs @@ -4,9 +4,9 @@ namespace ConsoleAppFramework; -internal class Emitter(Command command, WellKnownTypes wellKnownTypes) +internal class Emitter(WellKnownTypes wellKnownTypes) { - public string EmitRun(bool isRunAsync) + public string EmitRun(Command command, bool isRunAsync) { var hasCancellationToken = command.Parameters.Any(x => x.IsCancellationToken); var hasArgument = command.Parameters.Any(x => x.IsArgument); @@ -233,4 +233,73 @@ public string EmitRun(bool isRunAsync) return code; } + + public string EmitBuilder(Command[] commands, bool isRunAsync) // TODO: bool emitSync, emitAsync + { + // TODO: make Add -> make Run -> make RunAsync + // TODO: invoke RootCommand + var fields = new StringBuilder(); + var addCase = new StringBuilder(); + var runCommands = new StringBuilder(); + var delegateTypes = new List(); + + for (int i = 0; i < commands.Length; i++) + { + var command = commands[i]; + + var fieldType = command.BuildDelegateSignature(out var delegateType); + if (delegateType != null) + { + delegateTypes.Add(delegateType); + } + + fields.AppendLine($" {fieldType} command{i} = default!;"); + addCase.AppendLine($" case \"{command.CommandName}\":"); + addCase.AppendLine($" this.command{i} = command;"); + addCase.AppendLine($" break;"); + } + + var addCore = $$""" + [MethodImpl(MethodImplOptions.AggressiveInlining)] + partial void AddCore(string commandName, Delegate command) + { + switch (commandName) + { +{{addCase}} + default: + break; + } + } +"""; + + + // TODO: Emit help and version + var code = $$""" +partial struct ConsoleAppBuilder +{ +{{fields}} + +{{addCore}} + +{{runCommands}} + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + partial void RunCore(string[] args) + { + switch (commandName) + { + case "foo": + RunCommand1(args.AsSpan(1), command1); + break; + default: + break; + } + } +} + +{{string.Join(Environment.NewLine, delegateTypes.Distinct())}} +"""; + + return code; + } } diff --git a/src/ConsoleAppFramework5/Parser.cs b/src/ConsoleAppFramework5/Parser.cs index 7bc43f8..7df7019 100644 --- a/src/ConsoleAppFramework5/Parser.cs +++ b/src/ConsoleAppFramework5/Parser.cs @@ -7,44 +7,80 @@ namespace ConsoleAppFramework; internal class Parser(SourceProductionContext context, InvocationExpressionSyntax node, SemanticModel model, WellKnownTypes wellKnownTypes) { - public Command? ParseAndValidate() + public Command? ParseAndValidate() // for ConsoleApp.Run { var args = node.ArgumentList.Arguments; if (args.Count == 2) // 0 = args, 1 = lambda { - var lambda = args[1].Expression as ParenthesizedLambdaExpressionSyntax; - if (lambda == null) + var command = ExpressionToCommand(args[1].Expression, ""); // rootCommand = commandName = "" + if (command != null) { - if (args[1].Expression.IsKind(SyntaxKind.AddressOfExpression)) - { - var operand = (args[1].Expression as PrefixUnaryExpressionSyntax)!.Operand; + return ValidateCommand(command); + } + } - var methodSymbols = model.GetMemberGroup(operand); - if (methodSymbols.Length > 0 && methodSymbols[0] is IMethodSymbol methodSymbol) - { - return ValidateCommand(ParseFromMethodSymbol(methodSymbol, addressOf: true)); - } - } - else + context.ReportDiagnostic(DiagnosticDescriptors.RequireArgsAndMethod, node.GetLocation()); + return null; + } + + public Command? ParseAndValidateForCommand() // for ConsoleAppBuilder.Add + { + var args = node.ArgumentList.Arguments; + if (args.Count == 2) // 0 = string command, 1 = lambda + { + var commandName = args[0]; + + if (!commandName.Expression.IsKind(SyntaxKind.StringLiteralExpression)) + { + context.ReportDiagnostic(DiagnosticDescriptors.AddCommandMustBeStringLiteral, node.GetLocation()); + return null; + } + + var name = (commandName.Expression as LiteralExpressionSyntax)!.Token.ValueText; + var command = ExpressionToCommand(args[1].Expression, name); + if (command != null) + { + return ValidateCommand(command); + } + } + + context.ReportDiagnostic(DiagnosticDescriptors.RequireArgsAndMethod, node.GetLocation()); + return null; + } + + Command? ExpressionToCommand(ExpressionSyntax expression, string commandName) + { + var lambda = expression as ParenthesizedLambdaExpressionSyntax; + if (lambda == null) + { + if (expression.IsKind(SyntaxKind.AddressOfExpression)) + { + var operand = (expression as PrefixUnaryExpressionSyntax)!.Operand; + + var methodSymbols = model.GetMemberGroup(operand); + if (methodSymbols.Length > 0 && methodSymbols[0] is IMethodSymbol methodSymbol) { - var methodSymbols = model.GetMemberGroup(args[1].Expression); - if (methodSymbols.Length > 0 && methodSymbols[0] is IMethodSymbol methodSymbol) - { - return ValidateCommand(ParseFromMethodSymbol(methodSymbol, addressOf: false)); - } + return ParseFromMethodSymbol(methodSymbol, addressOf: true, commandName); } } else { - return ValidateCommand(ParseFromLambda(lambda)); + var methodSymbols = model.GetMemberGroup(expression); + if (methodSymbols.Length > 0 && methodSymbols[0] is IMethodSymbol methodSymbol) + { + return ParseFromMethodSymbol(methodSymbol, addressOf: false, commandName); + } } } + else + { + return ParseFromLambda(lambda, commandName); + } - context.ReportDiagnostic(DiagnosticDescriptors.RequireArgsOrMethod, node.GetLocation()); return null; } - Command? ParseFromLambda(ParenthesizedLambdaExpressionSyntax lambda) + Command? ParseFromLambda(ParenthesizedLambdaExpressionSyntax lambda, string commandName) { var isAsync = lambda.AsyncKeyword.IsKind(SyntaxKind.AsyncKeyword); @@ -210,9 +246,8 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta var cmd = new Command { - CommandName = "", + CommandName = commandName, IsAsync = isAsync, - IsRootCommand = true, IsVoid = isVoid, Parameters = parameters, MethodKind = MethodKind.Lambda, @@ -222,7 +257,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta return cmd; } - Command? ParseFromMethodSymbol(IMethodSymbol methodSymbol, bool addressOf) + Command? ParseFromMethodSymbol(IMethodSymbol methodSymbol, bool addressOf, string commandName) { var docComment = methodSymbol.DeclaringSyntaxReferences[0].GetSyntax().GetDocumentationCommentTriviaSyntax(); var summary = ""; @@ -322,9 +357,8 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta var cmd = new Command { - CommandName = "", + CommandName = commandName, IsAsync = isAsync, - IsRootCommand = true, IsVoid = isVoid, Parameters = parameters, MethodKind = addressOf ? MethodKind.FunctionPointer : MethodKind.Method, @@ -334,9 +368,8 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta return cmd; } - Command? ValidateCommand(Command? command) + Command? ValidateCommand(Command command) { - if (command == null) return null; var hasDiagnostic = false; // Sequential Argument From 4afa5647ce35adb1784425e1229e98ae3d81f338 Mon Sep 17 00:00:00 2001 From: neuecc Date: Mon, 20 May 2024 17:46:39 +0900 Subject: [PATCH 15/54] command builder sync/async --- sandbox/GeneratorSandbox/Program.cs | 165 +----------------- src/ConsoleAppFramework5/Command.cs | 10 +- .../ConsoleAppGenerator.cs | 110 ++++++++---- src/ConsoleAppFramework5/Emitter.cs | 87 ++++++--- src/ConsoleAppFramework5/Parser.cs | 8 +- 5 files changed, 155 insertions(+), 225 deletions(-) diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index 3e37e1d..44b2dda 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -17,7 +17,7 @@ using Microsoft.Extensions.DependencyInjection; -args = ["--x", "18", "--y", "aiueokakikukeko"]; // test. +args = ["foo", "--x", "1"]; // test. // ConsoleApp.Run(args, Run2); void Run2(int x, int yzzzz) { }; @@ -25,29 +25,23 @@ var builder = ConsoleApp.CreateBuilder(); -var aa = "foo"; - builder.Add("foo", (int x, int y) => { + Console.WriteLine($"foo: {(x, y)}"); }); -builder.Add("foo", (int x, int y) => +builder.Add("bar", async (int x, int y = 999) => { + await Task.Yield(); + Console.WriteLine($"bar: {(x, y)}"); }); - - - builder.Run(args); +await builder.RunAsync(args); -//builder.Add("foo", (int x, int y) => x + y); -//builder.Add("bar", RunRun); - -unsafe { ConsoleApp.Run(args, Run2); static void Run2(int x, [System.ComponentModel.DataAnnotations.Range(0, 10)] int y) { }; } - // var s = "foo"; // s.AsSpan().Split(',',). @@ -71,6 +65,7 @@ static async Task RunRun(int? x = null, string? y = null) } + static void Tests() where T : ISpanParsable { @@ -82,32 +77,6 @@ static void Tests() // constructor injection! -public class MyClass -{ - Action command1; // emit field - - public void Foo(string commandName, Delegate command) - { - // switch and cast - switch (commandName) - { - case "foo": - command1 = (Action)command; - break; - default: - break; - } - } - - //public void Foo(FooBar action2) - //{ - //} - - void Test() - { - Foo("takoyaki", (int x, int y = 10) => { }); - } -} public delegate void FooBar(int x, int y = 10); @@ -205,12 +174,6 @@ public static void Execute(int foo, CancellationToken cancellationToken) } -public class MyClass -{ - -} - - namespace Takoyaki { @@ -364,120 +327,6 @@ namespace ConsoleAppFramework partial class ConsoleApp { - //public 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); - // } - // } - //} - - - static Action? logErrorAction2; - public static Action LogError2 - { - get => logErrorAction2 ??= (static msg => Log(msg)); - set => logErrorAction2 = value; - } - - - // [MethodImpl - public static void Run2(string[] args, Action command) - { - - // command.Method - - - if (TryShowHelpOrVersion(args)) return; - - var arg0 = default(int); - var arg0Parsed = false; - var arg1 = default(int); - var arg1Parsed = false; - - try - { - for (int i = 0; i < args.Length; i++) - { - // add this block. - if (i == 0) - { - if (!int.TryParse(args[i], out arg0)) ThrowArgumentParseFailed("x", args[i]); // no++ - arg0Parsed = true; - continue; - } - - - var name = args[i]; - - switch (name) - { - case "--x": - if (!int.TryParse(args[++i], out arg0)) ThrowArgumentParseFailed("x", args[i]); - arg0Parsed = true; - break; - case "--y": - if (!int.TryParse(args[++i], out arg1)) ThrowArgumentParseFailed("y", args[i]); - arg1Parsed = true; - break; - - default: - if (string.Equals(name, "--x", StringComparison.OrdinalIgnoreCase)) - { - if (!int.TryParse(args[++i], out arg0)) ThrowArgumentParseFailed("x", args[i]); - arg0Parsed = true; - break; - } - if (string.Equals(name, "--y", StringComparison.OrdinalIgnoreCase)) - { - if (!int.TryParse(args[++i], out arg1)) ThrowArgumentParseFailed("y", args[i]); - arg1Parsed = true; - break; - } - - ThrowArgumentNameNotFound(name); - break; - } - } - if (!arg0Parsed) ThrowRequiredArgumentNotParsed("x"); - if (!arg1Parsed) ThrowRequiredArgumentNotParsed("y"); - - var validationContext = new ValidationContext(1000, null, null); - var parameters = command.GetMethodInfo().GetParameters(); - StringBuilder? errorMessages = null; - ValidateParameter(arg0, parameters[0], validationContext, ref errorMessages); - ValidateParameter(arg1, parameters[1], validationContext, ref errorMessages); - if (errorMessages != null) - { - throw new ValidationException(errorMessages.ToString()); - } - - command(arg0!, arg1!); - } - catch (Exception ex) - { - Environment.ExitCode = 1; - if (ex is ValidationException ve) - { - LogError(ex.Message); - } - else - { - LogError(ex.ToString()); - } - } - } } } diff --git a/src/ConsoleAppFramework5/Command.cs b/src/ConsoleAppFramework5/Command.cs index 570f7c3..67acb37 100644 --- a/src/ConsoleAppFramework5/Command.cs +++ b/src/ConsoleAppFramework5/Command.cs @@ -19,13 +19,17 @@ public record class Command public required CommandParameter[] Parameters { get; init; } public required string Description { get; init; } public required MethodKind MethodKind { get; init; } + public required bool DisableBuildDefaultValueDelgate { get; init; } public string BuildDelegateSignature(out string? delegateType) { - if (MethodKind == MethodKind.Lambda && Parameters.Any(x => x.HasDefaultValue)) + if (!DisableBuildDefaultValueDelgate) { - delegateType = BuildDelegateType("RunCommand"); - return "RunCommand"; + if (MethodKind == MethodKind.Lambda && Parameters.Any(x => x.HasDefaultValue)) + { + delegateType = BuildDelegateType("RunCommand"); + return "RunCommand"; + } } delegateType = null; diff --git a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs index 1ff5711..741ea2f 100644 --- a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs @@ -130,7 +130,7 @@ public static Task RunAsync(string[] args) return Task.CompletedTask; } - public static ConsoleAppBuilder CreateBuilder() + public static ConsoleAppBuilder CreateBuilder() => new ConsoleAppBuilder(); static void ThrowArgumentParseFailed(string argumentName, string value) { @@ -222,7 +222,7 @@ static void ValidateParameter(object? value, ParameterInfo parameter, Validation } [MethodImpl(MethodImplOptions.AggressiveInlining)] - static bool TryShowHelpOrVersion(string[] args) + static bool TryShowHelpOrVersion(ReadOnlySpan args) { if (args.Length == 0) { @@ -322,41 +322,45 @@ public void Dispose() timeoutCancellationTokenSource.Dispose(); } } -} -public partial struct ConsoleAppBuilder -{ - public void Add(string commandName, Delegate command) + internal partial struct ConsoleAppBuilder { - AddCore(commandName, command); - } + public ConsoleAppBuilder() + { + } - [Conditional("DEBUG")] - public void Add() { } + public void Add(string commandName, Delegate command) + { + AddCore(commandName, command); + } - [Conditional("DEBUG")] - public void Add(string commandName) { } + [System.Diagnostics.Conditional("DEBUG")] + public void Add() { } - public void Run(string[] args) - { - RunCore(args); - } + [System.Diagnostics.Conditional("DEBUG")] + public void Add(string commandName) { } - public Task RunAsync(string[] args) - { - Task? task = null; - RunAsyncCore(args, ref task!); - return task ?? Task.CompletedTask; - } + public void Run(string[] args) + { + RunCore(args); + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - partial void AddCore(string commandName, Delegate command); + public Task RunAsync(string[] args) + { + Task? task = null; + RunAsyncCore(args, ref task!); + return task ?? Task.CompletedTask; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - partial void RunCore(string[] args); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + partial void AddCore(string commandName, Delegate command); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - partial void RunAsyncCore(string[] args, ref Task result); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + partial void RunCore(string[] args); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + partial void RunAsyncCore(string[] args, ref Task result); + } } """; @@ -372,7 +376,7 @@ static void EmitConsoleAppRun(SourceProductionContext sourceProductionContext, ( var wellKnownTypes = new WellKnownTypes(model.Compilation); - var parser = new Parser(sourceProductionContext, node, model, wellKnownTypes); + var parser = new Parser(sourceProductionContext, node, model, wellKnownTypes, disableBuildDefaultValueDelgate: false); var command = parser.ParseAndValidate(); if (command == null) { @@ -401,6 +405,7 @@ static void EmitConsoleAppRun(SourceProductionContext sourceProductionContext, ( #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 namespace ConsoleAppFramework; @@ -429,7 +434,7 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex .Select(x => { // TODO: Add handling - var parser = new Parser(sourceProductionContext, x.Node, x.Model, wellKnownTypes); + var parser = new Parser(sourceProductionContext, x.Node, x.Model, wellKnownTypes, disableBuildDefaultValueDelgate: true); var command = parser.ParseAndValidateForCommand(); @@ -445,13 +450,52 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex return; } + var hasRun = group1["Run"].Any(); + var hasRunAsync = group1["RunAsync"].Any(); - var run = group1["Run"]; - var runAsync = group1["RunAsync"]; + if (!hasRun && !hasRunAsync) return; var emitter = new Emitter(wellKnownTypes); + var code = emitter.EmitBuilder(commands!, hasRun, hasRunAsync); - emitter.EmitBuilder(commands!, false); // TODO: isRunAsync + sourceProductionContext.AddSource("ConsoleApp.Builder.cs", $$""" +// +#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 + +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; + +internal static partial class ConsoleApp +{ + +{{code}} + +} +"""); } } \ No newline at end of file diff --git a/src/ConsoleAppFramework5/Emitter.cs b/src/ConsoleAppFramework5/Emitter.cs index 6ff098b..2c5ca4e 100644 --- a/src/ConsoleAppFramework5/Emitter.cs +++ b/src/ConsoleAppFramework5/Emitter.cs @@ -6,8 +6,9 @@ namespace ConsoleAppFramework; internal class Emitter(WellKnownTypes wellKnownTypes) { - public string EmitRun(Command command, bool isRunAsync) + public string EmitRun(Command command, bool isRunAsync, string? methodName = null) { + var emitForBuilder = methodName != null; var hasCancellationToken = command.Parameters.Any(x => x.IsCancellationToken); var hasArgument = command.Parameters.Any(x => x.IsArgument); var hasValidation = command.Parameters.Any(x => x.HasValidation); @@ -177,13 +178,15 @@ public string EmitRun(Command command, bool isRunAsync) } var returnType = isRunAsync ? "async Task" : "void"; - var methodName = isRunAsync ? "RunAsync" : "Run"; + var accessibility = !emitForBuilder ? "public" : "private"; + var argsType = !emitForBuilder ? "string[]" : (isRunAsync ? "string[]" : "ReadOnlySpan"); // NOTE: C# 13 will allow Span in async methods so can change to ReadOnlyMemory(and store .Span in local var) + methodName = methodName ?? (isRunAsync ? "RunAsync" : "Run"); var unsafeCode = (command.MethodKind == MethodKind.FunctionPointer) ? "unsafe " : ""; var commandMethodType = command.BuildDelegateSignature(out var delegateType); var code = $$""" - public static {{unsafeCode}}{{returnType}} {{methodName}}(string[] args, {{commandMethodType}} command) + {{accessibility}} static {{unsafeCode}}{{returnType}} {{methodName}}({{argsType}} args, {{commandMethodType}} command) { if (TryShowHelpOrVersion(args)) return; @@ -222,7 +225,7 @@ public string EmitRun(Command command, bool isRunAsync) } """; - if (delegateType != null) + if (delegateType != null && !emitForBuilder) { code += $$""" @@ -234,33 +237,46 @@ public string EmitRun(Command command, bool isRunAsync) return code; } - public string EmitBuilder(Command[] commands, bool isRunAsync) // TODO: bool emitSync, emitAsync + public string EmitBuilder(Command[] commands, bool emitSync, bool emitAsync) { // TODO: make Add -> make Run -> make RunAsync // TODO: invoke RootCommand var fields = new StringBuilder(); var addCase = new StringBuilder(); var runCommands = new StringBuilder(); - var delegateTypes = new List(); + var runAsyncCommands = new StringBuilder(); + var runCase = new StringBuilder(); + var runAsyncCase = new StringBuilder(); for (int i = 0; i < commands.Length; i++) { var command = commands[i]; + var fieldType = command.BuildDelegateSignature(out _); // for builder, always generate Action/Func. - var fieldType = command.BuildDelegateSignature(out var delegateType); - if (delegateType != null) - { - delegateTypes.Add(delegateType); - } - fields.AppendLine($" {fieldType} command{i} = default!;"); + addCase.AppendLine($" case \"{command.CommandName}\":"); - addCase.AppendLine($" this.command{i} = command;"); + addCase.AppendLine($" this.command{i} = Unsafe.As<{fieldType}>(command);"); addCase.AppendLine($" break;"); + + if (emitSync) + { + runCase.AppendLine($" case \"{command.CommandName}\":"); + runCase.AppendLine($" RunCommand{i}(args.AsSpan(1), command{i});"); + runCase.AppendLine($" break;"); + runCommands.AppendLine(EmitRun(command, false, $"RunCommand{i}")); + } + + if (emitAsync) + { + runAsyncCase.AppendLine($" case \"{command.CommandName}\":"); + runAsyncCase.AppendLine($" result = RunAsyncCommand{i}(args[1..], command{i});"); + runAsyncCase.AppendLine($" break;"); + runAsyncCommands.AppendLine(EmitRun(command, true, $"RunAsyncCommand{i}")); + } } var addCore = $$""" - [MethodImpl(MethodImplOptions.AggressiveInlining)] partial void AddCore(string commandName, Delegate command) { switch (commandName) @@ -272,6 +288,32 @@ partial void AddCore(string commandName, Delegate command) } """; + var runCore = $$""" + partial void RunCore(string[] args) + { + switch (args[0]) + { +{{runCase}} + default: + break; + } + } +"""; + + var runAsyncCore = $$""" + partial void RunAsyncCore(string[] args, ref Task result) + { + switch (args[0]) + { +{{runAsyncCase}} + default: + break; + } + } +"""; + + if (!emitSync) runCore = ""; + if (!emitAsync) runAsyncCore = ""; // TODO: Emit help and version var code = $$""" @@ -282,22 +324,11 @@ partial struct ConsoleAppBuilder {{addCore}} {{runCommands}} +{{runAsyncCommands}} - [MethodImpl(MethodImplOptions.AggressiveInlining)] - partial void RunCore(string[] args) - { - switch (commandName) - { - case "foo": - RunCommand1(args.AsSpan(1), command1); - break; - default: - break; - } - } +{{runCore}} +{{runAsyncCore}} } - -{{string.Join(Environment.NewLine, delegateTypes.Distinct())}} """; return code; diff --git a/src/ConsoleAppFramework5/Parser.cs b/src/ConsoleAppFramework5/Parser.cs index 7df7019..98650b7 100644 --- a/src/ConsoleAppFramework5/Parser.cs +++ b/src/ConsoleAppFramework5/Parser.cs @@ -5,7 +5,7 @@ namespace ConsoleAppFramework; -internal class Parser(SourceProductionContext context, InvocationExpressionSyntax node, SemanticModel model, WellKnownTypes wellKnownTypes) +internal class Parser(SourceProductionContext context, InvocationExpressionSyntax node, SemanticModel model, WellKnownTypes wellKnownTypes, bool disableBuildDefaultValueDelgate) { public Command? ParseAndValidate() // for ConsoleApp.Run { @@ -251,7 +251,8 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta IsVoid = isVoid, Parameters = parameters, MethodKind = MethodKind.Lambda, - Description = "" + Description = "", + DisableBuildDefaultValueDelgate = disableBuildDefaultValueDelgate }; return cmd; @@ -362,7 +363,8 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta IsVoid = isVoid, Parameters = parameters, MethodKind = addressOf ? MethodKind.FunctionPointer : MethodKind.Method, - Description = summary + Description = summary, + DisableBuildDefaultValueDelgate = disableBuildDefaultValueDelgate }; return cmd; From e92a26d6d726a7917fc70e8512d3278478e2ec9e Mon Sep 17 00:00:00 2001 From: neuecc Date: Mon, 20 May 2024 18:12:35 +0900 Subject: [PATCH 16/54] v --- sandbox/GeneratorSandbox/Program.cs | 2 +- src/ConsoleAppFramework5/DiagnosticDescriptors.cs | 14 +++++++------- src/ConsoleAppFramework5/Parser.cs | 2 ++ 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index 44b2dda..4b51195 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -36,7 +36,7 @@ Console.WriteLine($"bar: {(x, y)}"); }); -builder.Run(args); +//buq/ilder.Run(args); await builder.RunAsync(args); diff --git a/src/ConsoleAppFramework5/DiagnosticDescriptors.cs b/src/ConsoleAppFramework5/DiagnosticDescriptors.cs index 215e29d..c881212 100644 --- a/src/ConsoleAppFramework5/DiagnosticDescriptors.cs +++ b/src/ConsoleAppFramework5/DiagnosticDescriptors.cs @@ -28,33 +28,33 @@ public static DiagnosticDescriptor Create(int id, string title, string messageFo isEnabledByDefault: true); } - public static readonly DiagnosticDescriptor RequireArgsAndMethod = Create( + public static DiagnosticDescriptor RequireArgsAndMethod { get; } = Create( 1, "ConsoleApp.Run/RunAsync requires string[] args and lambda/method in arguments."); - public static readonly DiagnosticDescriptor ReturnTypeLambda = Create( + public static DiagnosticDescriptor ReturnTypeLambda { get; } = Create( 2, "Run lambda expressions return type must be void or int or async Task or async Task.", "Run lambda expressions return type must be void or int or async Task or async Task but returned '{0}'."); - public static readonly DiagnosticDescriptor ReturnTypeMethod = Create( + public static DiagnosticDescriptor ReturnTypeMethod { get; } = Create( 3, "Run referenced method return type must be void or int or async Task or async Task.", "Run referenced method return type must be void or int or async Task or async Task but returned '{0}'."); - public static readonly DiagnosticDescriptor SequentialArgument = Create( + public static DiagnosticDescriptor SequentialArgument { get; } = Create( 4, "All Argument parameters must be sequential from first."); - public static readonly DiagnosticDescriptor FunctionPointerCanNotHaveValidation = Create( + public static DiagnosticDescriptor FunctionPointerCanNotHaveValidation { get; } = Create( 5, "Function pointer can not have validation."); - public static readonly DiagnosticDescriptor RequireCommandAndMethod = Create( + public static DiagnosticDescriptor RequireCommandAndMethod { get; } = Create( 6, "ConsoleAppBuilder.Add requires string command and lambda/method in arguments or use Add."); - public static readonly DiagnosticDescriptor AddCommandMustBeStringLiteral = Create( + public static DiagnosticDescriptor AddCommandMustBeStringLiteral { get; } = Create( 6, "ConsoleAppBuilder.Add string command must be string literal."); } diff --git a/src/ConsoleAppFramework5/Parser.cs b/src/ConsoleAppFramework5/Parser.cs index 98650b7..06697da 100644 --- a/src/ConsoleAppFramework5/Parser.cs +++ b/src/ConsoleAppFramework5/Parser.cs @@ -17,6 +17,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta { return ValidateCommand(command); } + return null; } context.ReportDiagnostic(DiagnosticDescriptors.RequireArgsAndMethod, node.GetLocation()); @@ -42,6 +43,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta { return ValidateCommand(command); } + return null; } context.ReportDiagnostic(DiagnosticDescriptors.RequireArgsAndMethod, node.GetLocation()); From 0639ce2df0ce8dc6a3bc50ee91c01652ad72195a Mon Sep 17 00:00:00 2001 From: neuecc Date: Mon, 20 May 2024 19:38:36 +0900 Subject: [PATCH 17/54] g --- sandbox/GeneratorSandbox/Program.cs | 16 ++-------------- .../ConsoleAppGenerator.cs | 4 ++-- .../DiagnosticDescriptors.cs | 18 +++++++----------- .../CSharpGeneratorRunner.cs | 4 ++-- .../DiagnosticsTest.cs | 13 +++++++++++++ 5 files changed, 26 insertions(+), 29 deletions(-) diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index 4b51195..5525723 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -24,20 +24,8 @@ var builder = ConsoleApp.CreateBuilder(); - -builder.Add("foo", (int x, int y) => -{ - Console.WriteLine($"foo: {(x, y)}"); -}); - -builder.Add("bar", async (int x, int y = 999) => -{ - await Task.Yield(); - Console.WriteLine($"bar: {(x, y)}"); -}); - -//buq/ilder.Run(args); -await builder.RunAsync(args); +builder.Add("foo", (int x, int y) => { }); +builder.Run(args); diff --git a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs index 741ea2f..148a110 100644 --- a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs @@ -388,7 +388,7 @@ static void EmitConsoleAppRun(SourceProductionContext sourceProductionContext, ( var isRunAsync = ((node.Expression as MemberAccessExpressionSyntax)?.Name.Identifier.Text == "RunAsync"); var code = emitter.EmitRun(command, isRunAsync); - sourceProductionContext.AddSource("ConsoleApp.Run.cs", $$""" + sourceProductionContext.AddSource("ConsoleApp.Run.g.cs", $$""" // #nullable enable #pragma warning disable CS0108 // hides inherited member @@ -459,7 +459,7 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex var code = emitter.EmitBuilder(commands!, hasRun, hasRunAsync); - sourceProductionContext.AddSource("ConsoleApp.Builder.cs", $$""" + sourceProductionContext.AddSource("ConsoleApp.Builder.g.cs", $$""" // #nullable enable #pragma warning disable CS0108 // hides inherited member diff --git a/src/ConsoleAppFramework5/DiagnosticDescriptors.cs b/src/ConsoleAppFramework5/DiagnosticDescriptors.cs index c881212..284d7a2 100644 --- a/src/ConsoleAppFramework5/DiagnosticDescriptors.cs +++ b/src/ConsoleAppFramework5/DiagnosticDescriptors.cs @@ -38,21 +38,17 @@ public static DiagnosticDescriptor Create(int id, string title, string messageFo "Run lambda expressions return type must be void or int or async Task or async Task but returned '{0}'."); public static DiagnosticDescriptor ReturnTypeMethod { get; } = Create( - 3, - "Run referenced method return type must be void or int or async Task or async Task.", - "Run referenced method return type must be void or int or async Task or async Task but returned '{0}'."); + 3, + "Run referenced method return type must be void or int or async Task or async Task.", + "Run referenced method return type must be void or int or async Task or async Task but returned '{0}'."); public static DiagnosticDescriptor SequentialArgument { get; } = Create( - 4, - "All Argument parameters must be sequential from first."); + 4, + "All Argument parameters must be sequential from first."); public static DiagnosticDescriptor FunctionPointerCanNotHaveValidation { get; } = Create( - 5, - "Function pointer can not have validation."); - - public static DiagnosticDescriptor RequireCommandAndMethod { get; } = Create( - 6, - "ConsoleAppBuilder.Add requires string command and lambda/method in arguments or use Add."); + 5, + "Function pointer can not have validation."); public static DiagnosticDescriptor AddCommandMustBeStringLiteral { get; } = Create( 6, diff --git a/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs b/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs index 8cd9040..2f5255d 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs @@ -159,8 +159,8 @@ void OutputGeneratedCode(Compilation compilation) { foreach (var syntaxTree in compilation.SyntaxTrees) { - // only shows ConsoleApp.Run generated code - if (!syntaxTree.FilePath.Contains("ConsoleApp.Run.cs")) continue; + // only shows ConsoleApp.Run/Builder generated code + if (!syntaxTree.FilePath.Contains("g.cs")) continue; output.WriteLine(syntaxTree.ToString()); } } diff --git a/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs b/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs index e351508..4180148 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs @@ -86,4 +86,17 @@ public void FunctionPointerValidation() verifier.Ok("unsafe { ConsoleApp.Run(args, &Run2); static void Run2(int x, int y) { }; }"); } + + [Fact] + public void BuilderAddConst() + { + verifier.Ok(""" +var builder = ConsoleApp.CreateBuilder(); +builder.Add("foo", (int x, int y) => { } ); +builder.Run(args); +"""); + + + + } } From bbebec6e2f712e8f0fb7af9dcec7b418850bfc14 Mon Sep 17 00:00:00 2001 From: neuecc Date: Tue, 21 May 2024 02:18:51 +0900 Subject: [PATCH 18/54] test --- sandbox/GeneratorSandbox/Program.cs | 6 +- .../ConsoleAppGenerator.cs | 12 ++-- .../DiagnosticDescriptors.cs | 5 ++ src/ConsoleAppFramework5/Parser.cs | 2 +- .../ConsoleAppBuilderTest.cs | 58 +++++++++++++++++++ .../DiagnosticsTest.cs | 19 +++++- .../GlobalUsings.cs | 6 +- .../RunTest.cs | 11 ---- 8 files changed, 94 insertions(+), 25 deletions(-) create mode 100644 tests/ConsoleAppFramework.GeneratorTests/ConsoleAppBuilderTest.cs diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index 5525723..7f8e7e0 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -22,13 +22,9 @@ // ConsoleApp.Run(args, Run2); void Run2(int x, int yzzzz) { }; - var builder = ConsoleApp.CreateBuilder(); builder.Add("foo", (int x, int y) => { }); -builder.Run(args); - - - +// builder.Add("foo", (int x, int y) => { }); // var s = "foo"; // s.AsSpan().Split(',',). diff --git a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs index 148a110..6f51c13 100644 --- a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs @@ -429,22 +429,26 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex var group1 = generatorSyntaxContexts.ToLookup(x => x.Name); - // TODO: validation command name duplicate + var names = new HashSet(); var commands = group1["Add"] .Select(x => { // TODO: Add handling var parser = new Parser(sourceProductionContext, x.Node, x.Model, wellKnownTypes, disableBuildDefaultValueDelgate: true); - var command = parser.ParseAndValidateForCommand(); - // command.Parameters + // validation command name duplicate + if (command != null && !names.Add(command.CommandName)) + { + sourceProductionContext.ReportDiagnostic(DiagnosticDescriptors.DuplicateCommandName, x.Node.ArgumentList.Arguments[0].GetLocation(), command!.CommandName); + return null; + } return command; }) .ToArray(); - // don't emit if exists failure + // don't emit if exists failure(already reported error) if (commands.Any(x => x == null)) { return; diff --git a/src/ConsoleAppFramework5/DiagnosticDescriptors.cs b/src/ConsoleAppFramework5/DiagnosticDescriptors.cs index 284d7a2..aafc26b 100644 --- a/src/ConsoleAppFramework5/DiagnosticDescriptors.cs +++ b/src/ConsoleAppFramework5/DiagnosticDescriptors.cs @@ -53,4 +53,9 @@ public static DiagnosticDescriptor Create(int id, string title, string messageFo public static DiagnosticDescriptor AddCommandMustBeStringLiteral { get; } = Create( 6, "ConsoleAppBuilder.Add string command must be string literal."); + + public static DiagnosticDescriptor DuplicateCommandName { get; } = Create( + 7, + "Command name is duplicated.", + "Command name '{0}' is duplicated."); } diff --git a/src/ConsoleAppFramework5/Parser.cs b/src/ConsoleAppFramework5/Parser.cs index 06697da..f8bce91 100644 --- a/src/ConsoleAppFramework5/Parser.cs +++ b/src/ConsoleAppFramework5/Parser.cs @@ -33,7 +33,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta if (!commandName.Expression.IsKind(SyntaxKind.StringLiteralExpression)) { - context.ReportDiagnostic(DiagnosticDescriptors.AddCommandMustBeStringLiteral, node.GetLocation()); + context.ReportDiagnostic(DiagnosticDescriptors.AddCommandMustBeStringLiteral, commandName.GetLocation()); return null; } diff --git a/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppBuilderTest.cs b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppBuilderTest.cs new file mode 100644 index 0000000..7de071a --- /dev/null +++ b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppBuilderTest.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ConsoleAppFramework.GeneratorTests; + +public class ConsoleAppBuilderTest +{ + static string[] ToArgs(string args) => args.Split(' '); + + [Fact] + public void BuilderRun() + { + var code = """ +var builder = ConsoleApp.CreateBuilder(); +builder.Add("foo", (int x, int y) => { Console.Write(x + y); }); +builder.Add("bar", (int x, int y = 10) => { Console.Write(x + y); }); +builder.Add("baz", int (int x, string y) => { Console.Write(x + y); return 10; }); +builder.Add("boz", async Task (int x) => { await Task.Yield(); Console.Write(x * 2); }); +builder.Run(args); +"""; + + CSharpGeneratorRunner.CompileAndExecute(code, ToArgs("foo --x 10 --y 20")).Should().Be("30"); + CSharpGeneratorRunner.CompileAndExecute(code, ToArgs("bar --x 20 --y 30")).Should().Be("50"); + CSharpGeneratorRunner.CompileAndExecute(code, ToArgs("bar --x 20")).Should().Be("30"); + Environment.ExitCode.Should().Be(0); + CSharpGeneratorRunner.CompileAndExecute(code, ToArgs("baz --x 40 --y takoyaki")).Should().Be("40takoyaki"); + Environment.ExitCode.Should().Be(10); + Environment.ExitCode = 0; + + CSharpGeneratorRunner.CompileAndExecute(code, ToArgs("boz --x 40")).Should().Be("80"); + } + + [Fact] + public void BuilderRunAsync() + { + var code = """ +var builder = ConsoleApp.CreateBuilder(); +builder.Add("foo", (int x, int y) => { Console.Write(x + y); }); +builder.Add("bar", (int x, int y = 10) => { Console.Write(x + y); }); +builder.Add("baz", int (int x, string y) => { Console.Write(x + y); return 10; }); +builder.Add("boz", async Task (int x) => { await Task.Yield(); Console.Write(x * 2); }); +await builder.RunAsync(args); +"""; + + CSharpGeneratorRunner.CompileAndExecute(code, ToArgs("foo --x 10 --y 20")).Should().Be("30"); + CSharpGeneratorRunner.CompileAndExecute(code, ToArgs("bar --x 20 --y 30")).Should().Be("50"); + CSharpGeneratorRunner.CompileAndExecute(code, ToArgs("bar --x 20")).Should().Be("30"); + Environment.ExitCode.Should().Be(0); + CSharpGeneratorRunner.CompileAndExecute(code, ToArgs("baz --x 40 --y takoyaki")).Should().Be("40takoyaki"); + Environment.ExitCode.Should().Be(10); + Environment.ExitCode = 0; + + CSharpGeneratorRunner.CompileAndExecute(code, ToArgs("boz --x 40")).Should().Be("80"); + } +} diff --git a/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs b/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs index 4180148..d9b4e4f 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs @@ -88,15 +88,28 @@ public void FunctionPointerValidation() } [Fact] - public void BuilderAddConst() + public void BuilderAddConstCommandName() { + verifier.Verify(6,""" +var builder = ConsoleApp.CreateBuilder(); +var baz = "foo"; +builder.Add(baz, (int x, int y) => { } ); +""", "baz"); + verifier.Ok(""" var builder = ConsoleApp.CreateBuilder(); builder.Add("foo", (int x, int y) => { } ); builder.Run(args); """); + } - - + [Fact] + public void DuplicateCommandName() + { + verifier.Verify(7, """ +var builder = ConsoleApp.CreateBuilder(); +builder.Add("foo", (int x, int y) => { } ); +builder.Add("foo", (int x, int y) => { } ); +""", "\"foo\""); } } diff --git a/tests/ConsoleAppFramework.GeneratorTests/GlobalUsings.cs b/tests/ConsoleAppFramework.GeneratorTests/GlobalUsings.cs index 7fef4b0..3b07b9b 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/GlobalUsings.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/GlobalUsings.cs @@ -1,2 +1,6 @@ global using Xunit; -global using FluentAssertions; \ No newline at end of file +global using FluentAssertions; + +// CSharpGeneratorRunner.CompileAndExecute uses stdout hook(replace Console.Out) +// so can not work in parallel test +[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file diff --git a/tests/ConsoleAppFramework.GeneratorTests/RunTest.cs b/tests/ConsoleAppFramework.GeneratorTests/RunTest.cs index d34e40c..ccd0957 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/RunTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/RunTest.cs @@ -21,9 +21,6 @@ static string[] ToArgs(string args) public void SyncRun() { var result = CSharpGeneratorRunner.CompileAndExecute(""" -using System; -using ConsoleAppFramework; - ConsoleApp.Run(args, (int x, int y) => { Console.Write((x + y)); }); """, ToArgs("--x 10 --y 20")); @@ -34,10 +31,6 @@ public void SyncRun() public void ValidateOne() { var result = CSharpGeneratorRunner.CompileAndExecute(""" -using System; -using ConsoleAppFramework; -using System.ComponentModel.DataAnnotations; - ConsoleApp.Run(args, ([Range(1, 10)]int x, [Range(100, 200)]int y) => { Console.Write((x + y)); }); """, ToArgs("--x 100 --y 140")); @@ -57,10 +50,6 @@ The field x must be between 1 and 10. public void ValidateTwo() { var result = CSharpGeneratorRunner.CompileAndExecute(""" -using System; -using ConsoleAppFramework; -using System.ComponentModel.DataAnnotations; - ConsoleApp.Run(args, ([Range(1, 10)]int x, [Range(100, 200)]int y) => { Console.Write((x + y)); }); """, ToArgs("--x 100 --y 240")); From 462264bf1408e278fd946df43bf3e30c04375ebd Mon Sep 17 00:00:00 2001 From: neuecc Date: Tue, 21 May 2024 17:32:52 +0900 Subject: [PATCH 19/54] ya --- sandbox/GeneratorSandbox/Program.cs | 65 ++++++++++++++++- src/ConsoleAppFramework5/Command.cs | 43 +++++++++-- .../ConsoleAppFramework5.csproj | 6 +- .../ConsoleAppGenerator.cs | 45 +++++++++--- src/ConsoleAppFramework5/Emitter.cs | 54 +++++++++++--- src/ConsoleAppFramework5/Parser.cs | 72 ++++++++++++++++++- src/ConsoleAppFramework5/WellKnownTypes.cs | 7 +- .../ConsoleAppBuilderTest.cs | 41 +++++++++++ 8 files changed, 302 insertions(+), 31 deletions(-) diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index 7f8e7e0..ab50f8e 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -17,14 +17,19 @@ using Microsoft.Extensions.DependencyInjection; -args = ["foo", "--x", "1"]; // test. +args = ["do"]; // test. + + // ConsoleApp.Run(args, Run2); void Run2(int x, int yzzzz) { }; + var builder = ConsoleApp.CreateBuilder(); -builder.Add("foo", (int x, int y) => { }); -// builder.Add("foo", (int x, int y) => { }); +builder.Add(); + + +builder.Run(args); // var s = "foo"; // s.AsSpan().Split(',',). @@ -57,7 +62,61 @@ static void Tests() } +public class MyClass +{ + public void Do() + { + Console.Write("yeah"); + } + + public void Sum(int x, int y) + { + Console.Write(x + y); + } + + public void Echo(string msg) + { + Console.Write(msg); + } + + void Echo() + { + } + public static void Sum() + { + } +} + +public class MyCommands : IDisposable +{ + public int MyProperty { get; set; } + + public void Foo(int x) + { + Console.WriteLine("MyCommands.Foo:" + x); + } + + public int Boo() => 1; + + public static void Tako() + { + } + + private void Bar() + { + } + + public ValueTask DisposeAsync() + { + throw new NotImplementedException(); + } + + public void Dispose() + { + Console.WriteLine("Disposed"); + } +} // constructor injection! diff --git a/src/ConsoleAppFramework5/Command.cs b/src/ConsoleAppFramework5/Command.cs index 67acb37..985ee56 100644 --- a/src/ConsoleAppFramework5/Command.cs +++ b/src/ConsoleAppFramework5/Command.cs @@ -1,4 +1,6 @@ using Microsoft.CodeAnalysis; +using System; +using System.Data.Common; using System.Diagnostics.CodeAnalysis; using System.Reflection.Metadata; using System.Text; @@ -10,6 +12,13 @@ public enum MethodKind Lambda, Method, FunctionPointer } +public enum DelegateBuildType +{ + MakeDelegateWhenHasDefaultValue, + OnlyActionFunc, + None +} + public record class Command { public required bool IsAsync { get; init; } // Task or Task @@ -19,11 +28,12 @@ public record class Command public required CommandParameter[] Parameters { get; init; } public required string Description { get; init; } public required MethodKind MethodKind { get; init; } - public required bool DisableBuildDefaultValueDelgate { get; init; } + public required DelegateBuildType DelegateBuildType { get; init; } + public CommandMethodInfo? CommandMethodInfo { get; set; } // can set...! public string BuildDelegateSignature(out string? delegateType) { - if (!DisableBuildDefaultValueDelgate) + if (DelegateBuildType == DelegateBuildType.MakeDelegateWhenHasDefaultValue) { if (MethodKind == MethodKind.Lambda && Parameters.Any(x => x.HasDefaultValue)) { @@ -33,6 +43,12 @@ public string BuildDelegateSignature(out string? delegateType) } delegateType = null; + + if (DelegateBuildType == DelegateBuildType.None) + { + return ""; + } + if (MethodKind == MethodKind.FunctionPointer) return BuildFunctionPointerDelegateSignature(); if (IsAsync) @@ -137,10 +153,7 @@ public record class CommandParameter public required bool IsFromServices { get; init; } public required bool IsCancellationToken { get; init; } public bool IsParsable => !(IsFromServices || IsCancellationToken); - - // 追加!コンパイルエラーありがたい! public required bool HasValidation { get; init; } - public required int ArgumentIndex { get; init; } // -1 is not Argument, other than marked as [Argument] public bool IsArgument => ArgumentIndex != -1; public required string[] Aliases { get; init; } @@ -285,4 +298,24 @@ public override string ToString() return sb.ToString(); } +} + +public record class CommandMethodInfo +{ + public required string TypeFullName { get; init; } + public required string MethodName { get; init; } + public required ITypeSymbol[] ConstructorParameterTypes { get; init; } + public required bool IsIDisposable { get; init; } + public required bool IsIAsyncDisposable { get; init; } + + public string BuildNew() + { + var p = ConstructorParameterTypes.Select(parameter => + { + var type = parameter.ToFullyQualifiedFormatDisplayString(); + return $"({type})ServiceProvider!.GetService(typeof({type}))!"; + }); + + return $"new {TypeFullName}({string.Join(", ", p)})"; + } } \ No newline at end of file diff --git a/src/ConsoleAppFramework5/ConsoleAppFramework5.csproj b/src/ConsoleAppFramework5/ConsoleAppFramework5.csproj index d219b7e..b6c00c5 100644 --- a/src/ConsoleAppFramework5/ConsoleAppFramework5.csproj +++ b/src/ConsoleAppFramework5/ConsoleAppFramework5.csproj @@ -1,4 +1,6 @@ - + + + netstandard2.0 @@ -20,3 +22,5 @@ + + diff --git a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs index 6f51c13..30b861e 100644 --- a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs @@ -338,7 +338,7 @@ public void Add(string commandName, Delegate command) public void Add() { } [System.Diagnostics.Conditional("DEBUG")] - public void Add(string commandName) { } + public void Add(string commandPath) { } public void Run(string[] args) { @@ -376,7 +376,7 @@ static void EmitConsoleAppRun(SourceProductionContext sourceProductionContext, ( var wellKnownTypes = new WellKnownTypes(model.Compilation); - var parser = new Parser(sourceProductionContext, node, model, wellKnownTypes, disableBuildDefaultValueDelgate: false); + var parser = new Parser(sourceProductionContext, node, model, wellKnownTypes, DelegateBuildType.MakeDelegateWhenHasDefaultValue); var command = parser.ParseAndValidate(); if (command == null) { @@ -427,14 +427,21 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex var wellKnownTypes = new WellKnownTypes(model.Compilation); - var group1 = generatorSyntaxContexts.ToLookup(x => x.Name); + var group1 = generatorSyntaxContexts.ToLookup(x => + { + if (x.Name == "Add" && ((x.Node.Expression as MemberAccessExpressionSyntax)?.Name.IsKind(SyntaxKind.GenericName) ?? false)) + { + return "Add"; + } + + return x.Name; + }); var names = new HashSet(); - var commands = group1["Add"] + var commands1 = group1["Add"] .Select(x => { - // TODO: Add handling - var parser = new Parser(sourceProductionContext, x.Node, x.Model, wellKnownTypes, disableBuildDefaultValueDelgate: true); + var parser = new Parser(sourceProductionContext, x.Node, x.Model, wellKnownTypes, DelegateBuildType.OnlyActionFunc); var command = parser.ParseAndValidateForCommand(); // validation command name duplicate @@ -445,8 +452,28 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex } return command; - }) - .ToArray(); + }); + + var commands2 = group1["Add"] + .SelectMany(x => + { + var parser = new Parser(sourceProductionContext, x.Node, x.Model, wellKnownTypes, DelegateBuildType.None); + var commands = parser.ParseForBuilderClassRegistration(); + + // validation command name duplicate? + foreach (var command in commands) + { + if (command != null && !names.Add(command.CommandName)) + { + sourceProductionContext.ReportDiagnostic(DiagnosticDescriptors.DuplicateCommandName, x.Node.GetLocation(), command!.CommandName); + return [null]; + } + } + + return commands; + }); + + var commands = commands1.Concat(commands2).ToArray(); // don't emit if exists failure(already reported error) if (commands.Any(x => x == null)) @@ -454,6 +481,8 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex return; } + if (commands.Length == 0) return; + var hasRun = group1["Run"].Any(); var hasRunAsync = group1["RunAsync"].Any(); diff --git a/src/ConsoleAppFramework5/Emitter.cs b/src/ConsoleAppFramework5/Emitter.cs index 2c5ca4e..49028b9 100644 --- a/src/ConsoleAppFramework5/Emitter.cs +++ b/src/ConsoleAppFramework5/Emitter.cs @@ -141,8 +141,33 @@ public string EmitRun(Command command, bool isRunAsync, string? methodName = nul } // invoke for sync/async, void/int + var invoke = new StringBuilder(); var methodArguments = string.Join(", ", command.Parameters.Select((x, i) => $"arg{i}!")); - var invokeCommand = $"command({methodArguments})"; + string invokeCommand; + if (command.CommandMethodInfo == null) + { + invokeCommand = $"command({methodArguments})"; + } + else + { + var usingInstance = (isRunAsync, command.CommandMethodInfo.IsIDisposable, command.CommandMethodInfo.IsIAsyncDisposable) switch + { + // awaitable + (true, true, true) => "await using ", + (true, true, false) => "using ", + (true, false, true) => "await using ", + (true, false, false) => "", + // sync + (false, true, true) => "using ", + (false, true, false) => "using ", + (false, false, true) => "", // IAsyncDisposable but sync, can't call disposeasync...... + (false, false, false) => "" + }; + + invoke.AppendLine($" {usingInstance}var instance = {command.CommandMethodInfo.BuildNew()};"); + invokeCommand = $"instance.{command.CommandMethodInfo.MethodName}({methodArguments})"; + } + if (hasCancellationToken) { invokeCommand = $"Task.Run(() => {invokeCommand}).WaitAsync(posixSignalHandler.TimeoutToken)"; @@ -159,7 +184,6 @@ public string EmitRun(Command command, bool isRunAsync, string? methodName = nul } } - var invoke = new StringBuilder(); if (command.IsVoid) { invoke.AppendLine($" {invokeCommand};"); @@ -184,9 +208,13 @@ public string EmitRun(Command command, bool isRunAsync, string? methodName = nul var unsafeCode = (command.MethodKind == MethodKind.FunctionPointer) ? "unsafe " : ""; var commandMethodType = command.BuildDelegateSignature(out var delegateType); + if (commandMethodType != "") + { + commandMethodType = $", {commandMethodType} command"; + } var code = $$""" - {{accessibility}} static {{unsafeCode}}{{returnType}} {{methodName}}({{argsType}} args, {{commandMethodType}} command) + {{accessibility}} static {{unsafeCode}}{{returnType}} {{methodName}}({{argsType}} args{{commandMethodType}}) { if (TryShowHelpOrVersion(args)) return; @@ -251,18 +279,24 @@ public string EmitBuilder(Command[] commands, bool emitSync, bool emitAsync) for (int i = 0; i < commands.Length; i++) { var command = commands[i]; - var fieldType = command.BuildDelegateSignature(out _); // for builder, always generate Action/Func. - fields.AppendLine($" {fieldType} command{i} = default!;"); + string commandArgs = ""; + if (command.DelegateBuildType != DelegateBuildType.None) + { + var fieldType = command.BuildDelegateSignature(out _); // for builder, always generate Action/Func. + fields.AppendLine($" {fieldType} command{i} = default!;"); + + addCase.AppendLine($" case \"{command.CommandName}\":"); + addCase.AppendLine($" this.command{i} = Unsafe.As<{fieldType}>(command);"); + addCase.AppendLine($" break;"); - addCase.AppendLine($" case \"{command.CommandName}\":"); - addCase.AppendLine($" this.command{i} = Unsafe.As<{fieldType}>(command);"); - addCase.AppendLine($" break;"); + commandArgs = $", command{i}"; + } if (emitSync) { runCase.AppendLine($" case \"{command.CommandName}\":"); - runCase.AppendLine($" RunCommand{i}(args.AsSpan(1), command{i});"); + runCase.AppendLine($" RunCommand{i}(args.AsSpan(1){commandArgs});"); runCase.AppendLine($" break;"); runCommands.AppendLine(EmitRun(command, false, $"RunCommand{i}")); } @@ -270,7 +304,7 @@ public string EmitBuilder(Command[] commands, bool emitSync, bool emitAsync) if (emitAsync) { runAsyncCase.AppendLine($" case \"{command.CommandName}\":"); - runAsyncCase.AppendLine($" result = RunAsyncCommand{i}(args[1..], command{i});"); + runAsyncCase.AppendLine($" result = RunAsyncCommand{i}(args[1..]{commandArgs});"); runAsyncCase.AppendLine($" break;"); runAsyncCommands.AppendLine(EmitRun(command, true, $"RunAsyncCommand{i}")); } diff --git a/src/ConsoleAppFramework5/Parser.cs b/src/ConsoleAppFramework5/Parser.cs index f8bce91..2b1cdef 100644 --- a/src/ConsoleAppFramework5/Parser.cs +++ b/src/ConsoleAppFramework5/Parser.cs @@ -5,7 +5,7 @@ namespace ConsoleAppFramework; -internal class Parser(SourceProductionContext context, InvocationExpressionSyntax node, SemanticModel model, WellKnownTypes wellKnownTypes, bool disableBuildDefaultValueDelgate) +internal class Parser(SourceProductionContext context, InvocationExpressionSyntax node, SemanticModel model, WellKnownTypes wellKnownTypes, DelegateBuildType delegateBuildType) { public Command? ParseAndValidate() // for ConsoleApp.Run { @@ -26,6 +26,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta public Command? ParseAndValidateForCommand() // for ConsoleAppBuilder.Add { + // Add(string commandName) var args = node.ArgumentList.Arguments; if (args.Count == 2) // 0 = string command, 1 = lambda { @@ -50,6 +51,70 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta return null; } + public Command?[] ParseForBuilderClassRegistration() + { + // Add + var genericName = (node.Expression as MemberAccessExpressionSyntax)?.Name as GenericNameSyntax; + var genericType = genericName!.TypeArgumentList.Arguments[0]; + + // TODO: Add(string commandPath) + + // T + var type = model.GetTypeInfo(genericType).Type!; + + if (type.IsStatic || type.IsAbstract) + { + // TODO: validation + } + + var publicMethods = type.GetMembers() + .Where(x => x.DeclaredAccessibility == Accessibility.Public) + .OfType() + .Where(x => x.DeclaredAccessibility == Accessibility.Public && !x.IsStatic) + .Where(x => x.MethodKind == Microsoft.CodeAnalysis.MethodKind.Ordinary) + .Where(x => !(x.Name is "Dispose" or "DisposeAsync")) + .ToArray(); + + var publicConstructors = type.GetMembers() + .OfType() + .Where(x => x.MethodKind == Microsoft.CodeAnalysis.MethodKind.Constructor && x.DeclaredAccessibility == Accessibility.Public) + .ToArray(); + + if (publicMethods.Length == 0) + { + // TODO: validation + } + + if (publicConstructors.Length != 1) + { + // TODO: validation + } + + var hasIDisposable = type.AllInterfaces.Any(x => SymbolEqualityComparer.Default.Equals(x, wellKnownTypes.IDisposable)); + var hasIAsyncDisposable = type.AllInterfaces.Any(x => SymbolEqualityComparer.Default.Equals(x, wellKnownTypes.IAsyncDisposable)); + + var methodInfoBase = new CommandMethodInfo + { + TypeFullName = type.ToFullyQualifiedFormatDisplayString(), + IsIDisposable = hasIDisposable, + IsIAsyncDisposable = hasIAsyncDisposable, + ConstructorParameterTypes = publicConstructors[0].Parameters.Select(x => x.Type).ToArray(), + MethodName = "", // without methodname + }; + + return publicMethods + .Select(x => + { + // TODO: commandName convert to snake-case + var command = ParseFromMethodSymbol(x, false, x.Name.ToLowerInvariant()); + if (command == null) return null; + + command.CommandMethodInfo = methodInfoBase with { MethodName = x.Name }; + return command; + }) + .ToArray(); + } + Command? ExpressionToCommand(ExpressionSyntax expression, string commandName) { var lambda = expression as ParenthesizedLambdaExpressionSyntax; @@ -254,7 +319,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta Parameters = parameters, MethodKind = MethodKind.Lambda, Description = "", - DisableBuildDefaultValueDelgate = disableBuildDefaultValueDelgate + DelegateBuildType = delegateBuildType }; return cmd; @@ -297,6 +362,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta } else { + // TODO: Arguments[1] is dangerous... context.ReportDiagnostic(DiagnosticDescriptors.ReturnTypeMethod, node.ArgumentList.Arguments[1].GetLocation(), methodSymbol.ReturnType); return null; } @@ -366,7 +432,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta Parameters = parameters, MethodKind = addressOf ? MethodKind.FunctionPointer : MethodKind.Method, Description = summary, - DisableBuildDefaultValueDelgate = disableBuildDefaultValueDelgate + DelegateBuildType = delegateBuildType }; return cmd; diff --git a/src/ConsoleAppFramework5/WellKnownTypes.cs b/src/ConsoleAppFramework5/WellKnownTypes.cs index 2442a0f..c758e53 100644 --- a/src/ConsoleAppFramework5/WellKnownTypes.cs +++ b/src/ConsoleAppFramework5/WellKnownTypes.cs @@ -25,9 +25,14 @@ public class WellKnownTypes(Compilation compilation) INamedTypeSymbol? task_T; public INamedTypeSymbol Task_T => task_T ??= GetTypeByMetadataName("System.Threading.Tasks.Task`1"); + INamedTypeSymbol? disposable; + public INamedTypeSymbol IDisposable => disposable ??= GetTypeByMetadataName("System.IDisposable"); + + INamedTypeSymbol? asyncDisposable; + public INamedTypeSymbol IAsyncDisposable => asyncDisposable ??= GetTypeByMetadataName("System.IAsyncDisposable"); + public bool HasTryParse(ITypeSymbol type) { - if (SymbolEqualityComparer.Default.Equals(type, DateTimeOffset) || SymbolEqualityComparer.Default.Equals(type, Guid) || SymbolEqualityComparer.Default.Equals(type, Version) diff --git a/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppBuilderTest.cs b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppBuilderTest.cs index 7de071a..76aa84a 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppBuilderTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppBuilderTest.cs @@ -55,4 +55,45 @@ public void BuilderRunAsync() CSharpGeneratorRunner.CompileAndExecute(code, ToArgs("boz --x 40")).Should().Be("80"); } + + [Fact] + public void AddClass() + { + var code = """ +var builder = ConsoleApp.CreateBuilder(); +builder.Add(); +await builder.RunAsync(args); + +public class MyClass +{ + public void Do() + { + Console.Write("yeah"); + } + + public void Sum(int x, int y) + { + Console.Write(x + y); + } + + public void Echo(string msg) + { + Console.Write(msg); + } + + void Echo() + { + } + + public static void Sum() + { + } } +"""; + + CSharpGeneratorRunner.CompileAndExecute(code, ToArgs("do")).Should().Be("yeah"); + + } +} + + From c9901c22e587af47009627e83f6424da253265bc Mon Sep 17 00:00:00 2001 From: neuecc Date: Wed, 22 May 2024 02:58:06 +0900 Subject: [PATCH 20/54] y --- sandbox/CliFrameworkBenchmark/Benchmark.cs | 92 ++++++++---- .../CliFrameworkBenchmark.csproj | 1 + .../Commands/PowerArgsCommand.cs | 24 +-- .../Commands/SpectreConsoleCliCommand.cs | 24 +++ .../Commands/SystemCommandLineCommand.cs | 25 +++- sandbox/CliFrameworkBenchmark/Program.cs | 3 +- sandbox/GeneratorSandbox/Program.cs | 141 +++++++++++++++++- .../ConsoleAppGenerator.cs | 6 +- src/ConsoleAppFramework5/Emitter.cs | 3 +- .../CSharpGeneratorRunner.cs | 24 ++- .../ConsoleAppBuilderTest.cs | 135 +++++++++++++++-- .../RunTest.cs | 29 ++-- .../Integration/CommandFilterTest.cs | 60 ++++---- 13 files changed, 448 insertions(+), 119 deletions(-) create mode 100644 sandbox/CliFrameworkBenchmark/Commands/SpectreConsoleCliCommand.cs diff --git a/sandbox/CliFrameworkBenchmark/Benchmark.cs b/sandbox/CliFrameworkBenchmark/Benchmark.cs index 14cfe20..b0d4b6a 100644 --- a/sandbox/CliFrameworkBenchmark/Benchmark.cs +++ b/sandbox/CliFrameworkBenchmark/Benchmark.cs @@ -2,16 +2,21 @@ // https://github.com/Tyrrrz/CliFx/tree/master/CliFx.Benchmarks/ using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Engines; using BenchmarkDotNet.Order; using CliFx; using Cocona.Benchmark.External.Commands; using CommandLine; using ConsoleAppFramework; +using PowerArgs; +using Spectre.Console.Cli; +using System.ComponentModel.DataAnnotations.Schema; +using BenchmarkDotNet.Columns; namespace Cocona.Benchmark.External; -// [SimpleJob] -[ShortRunJob] +// use ColdStart strategy to measure startup time evaluation +[SimpleJob(RunStrategy.ColdStart, launchCount: 1, warmupCount: 0, iterationCount: 1, invocationCount: 1)] [RankColumn] [MemoryDiagnoser] [Orderer(SummaryOrderPolicy.FastestToSlowest)] @@ -20,57 +25,78 @@ public class Benchmark private static readonly string[] Arguments = { "--str", "hello world", "-i", "13", "-b" }; [Benchmark(Description = "Cocona.Lite", Baseline = true)] - public async Task ExecuteWithCoconaLite() => - await Cocona.CoconaLiteApp.RunAsync(Arguments); + public void ExecuteWithCoconaLite() + { + Cocona.CoconaLiteApp.Run(Arguments); + } [Benchmark(Description = "Cocona")] - public async ValueTask ExecuteWithCocona() => - await Cocona.CoconaApp.RunAsync(Arguments); + public void ExecuteWithCocona() + { + Cocona.CoconaApp.Run(Arguments); + } //[Benchmark(Description = "ConsoleAppFramework")] //public async ValueTask ExecuteWithConsoleAppFramework() => // await Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(Arguments); [Benchmark(Description = "CliFx")] - public async ValueTask ExecuteWithCliFx() => - await new CliApplicationBuilder().AddCommand(typeof(CliFxCommand)).Build().RunAsync(Arguments); + public ValueTask ExecuteWithCliFx() + { + return new CliApplicationBuilder().AddCommand(typeof(CliFxCommand)).Build().RunAsync(Arguments); + } [Benchmark(Description = "System.CommandLine")] - public async Task ExecuteWithSystemCommandLine() => - await new SystemCommandLineCommand().ExecuteAsync(Arguments); + public int ExecuteWithSystemCommandLine() + { + return SystemCommandLineCommand.Execute(Arguments); + } - [Benchmark(Description = "McMaster.Extensions.CommandLineUtils")] - public int ExecuteWithMcMaster() => - McMaster.Extensions.CommandLineUtils.CommandLineApplication.Execute(Arguments); + //[Benchmark(Description = "McMaster.Extensions.CommandLineUtils")] + //public int ExecuteWithMcMaster() => + // McMaster.Extensions.CommandLineUtils.CommandLineApplication.Execute(Arguments); - [Benchmark(Description = "CommandLineParser")] - public void ExecuteWithCommandLineParser() => - new Parser() - .ParseArguments(Arguments, typeof(CommandLineParserCommand)) - .WithParsed(c => c.Execute()); + //[Benchmark(Description = "CommandLineParser")] + //public void ExecuteWithCommandLineParser() => + // new Parser() + // .ParseArguments(Arguments, typeof(CommandLineParserCommand)) + // .WithParsed(c => c.Execute()); - [Benchmark(Description = "PowerArgs")] - public void ExecuteWithPowerArgs() => - PowerArgs.Args.InvokeMain(Arguments); + //[Benchmark(Description = "PowerArgs")] + //public void ExecuteWithPowerArgs() => + // PowerArgs.Args.InvokeMain(Arguments); - [Benchmark(Description = "Clipr")] - public void ExecuteWithClipr() => - clipr.CliParser.Parse(Arguments).Execute(); + //[Benchmark(Description = "Clipr")] + //public void ExecuteWithClipr() => + // clipr.CliParser.Parse(Arguments).Execute(); + //[Benchmark(Description = "ConsoleAppFramework v5")] + //public void ExecuteConsoleAppFramework5() + //{ + // ConsoleApp.Run(Arguments, ConsoleAppFrameworkCommand.Execute); + //} + [Benchmark(Description = "ConsoleAppFramework v5")] - public void ExecuteConsoleAppFramework5() + public unsafe void ExecuteConsoleAppFramework() { - ConsoleApp.Run(Arguments, ConsoleAppFrameworkCommand.Execute); + ConsoleApp.Run(Arguments, &ConsoleAppFrameworkCommand.Execute); } - //[Benchmark(Description = "ConsoleAppFramework v5(FP)")] - //public unsafe void ExecuteConsoleAppFramework5_2() + // for alpha testing + //private static readonly string[] TempArguments = { "", "--str", "hello world", "-i", "13", "-b" }; + //[Benchmark(Description = "ConsoleAppFramework.Builder")] + //public unsafe void ExecuteConsoleAppFrameworkBuilder() //{ - // ConsoleApp.Run(Arguments2, &Run); - - // static void Run(string str, int i, bool b) - // { - // } + // var builder = ConsoleApp.CreateBuilder(); + // builder.Add("", ConsoleAppFrameworkCommand.Execute); + // builder.Run(TempArguments); //} + + [Benchmark(Description = "Spectre.Console.Cli")] + public void ExecuteSpectreConsoleCli() + { + var app = new CommandApp(); + app.Run(Arguments); + } } \ No newline at end of file diff --git a/sandbox/CliFrameworkBenchmark/CliFrameworkBenchmark.csproj b/sandbox/CliFrameworkBenchmark/CliFrameworkBenchmark.csproj index a744b05..2294410 100644 --- a/sandbox/CliFrameworkBenchmark/CliFrameworkBenchmark.csproj +++ b/sandbox/CliFrameworkBenchmark/CliFrameworkBenchmark.csproj @@ -18,6 +18,7 @@ + diff --git a/sandbox/CliFrameworkBenchmark/Commands/PowerArgsCommand.cs b/sandbox/CliFrameworkBenchmark/Commands/PowerArgsCommand.cs index ce18da5..33d14c2 100644 --- a/sandbox/CliFrameworkBenchmark/Commands/PowerArgsCommand.cs +++ b/sandbox/CliFrameworkBenchmark/Commands/PowerArgsCommand.cs @@ -2,18 +2,18 @@ namespace Cocona.Benchmark.External.Commands; -public class PowerArgsCommand -{ - [ArgShortcut("--str"), ArgShortcut("-s")] - public string? StrOption { get; set; } +//public class PowerArgsCommand +//{ +// [ArgShortcut("--str"), ArgShortcut("-s")] +// public string? StrOption { get; set; } - [ArgShortcut("--int"), ArgShortcut("-i")] - public int IntOption { get; set; } +// [ArgShortcut("--int"), ArgShortcut("-i")] +// public int IntOption { get; set; } - [ArgShortcut("--bool"), ArgShortcut("-b")] - public bool BoolOption { get; set; } +// [ArgShortcut("--bool"), ArgShortcut("-b")] +// public bool BoolOption { get; set; } - public void Main() - { - } -} \ No newline at end of file +// public void Main() +// { +// } +//} \ No newline at end of file diff --git a/sandbox/CliFrameworkBenchmark/Commands/SpectreConsoleCliCommand.cs b/sandbox/CliFrameworkBenchmark/Commands/SpectreConsoleCliCommand.cs new file mode 100644 index 0000000..0a8d4c2 --- /dev/null +++ b/sandbox/CliFrameworkBenchmark/Commands/SpectreConsoleCliCommand.cs @@ -0,0 +1,24 @@ +using Spectre.Console.Cli; +using System.ComponentModel; + +namespace Cocona.Benchmark.External.Commands; + +public class SpectreConsoleCliCommand : Command +{ + public sealed class Settings : CommandSettings + { + [CommandOption("-s")] + public string? strOption { get; init; } + + [CommandOption("-i")] + public int intOption { get; init; } + + [CommandOption("-b")] + public bool boolOption { get; init; } + } + + public override int Execute(CommandContext context, Settings settings) + { + return 0; + } +} \ No newline at end of file diff --git a/sandbox/CliFrameworkBenchmark/Commands/SystemCommandLineCommand.cs b/sandbox/CliFrameworkBenchmark/Commands/SystemCommandLineCommand.cs index 53964e6..ca5a114 100644 --- a/sandbox/CliFrameworkBenchmark/Commands/SystemCommandLineCommand.cs +++ b/sandbox/CliFrameworkBenchmark/Commands/SystemCommandLineCommand.cs @@ -7,7 +7,7 @@ public class SystemCommandLineCommand { public static int ExecuteHandler(string s, int i, bool b) => 0; - public Task ExecuteAsync(string[] args) + public static int Execute(string[] args) { var command = new RootCommand { @@ -25,8 +25,29 @@ public Task ExecuteAsync(string[] args) } }; - command.Handler = CommandHandler.Create(typeof(SystemCommandLineCommand).GetMethod(nameof(ExecuteHandler))); + command.Handler = CommandHandler.Create(ExecuteHandler); + return command.Invoke(args); + } + + public static Task ExecuteAsync(string[] args) + { + var command = new RootCommand + { + new Option(new[] {"--str", "-s"}) + { + Argument = new Argument() + }, + new Option(new[] {"--int", "-i"}) + { + Argument = new Argument() + }, + new Option(new[] {"--bool", "-b"}) + { + Argument = new Argument() + } + }; + command.Handler = CommandHandler.Create(ExecuteHandler); return command.InvokeAsync(args); } } \ No newline at end of file diff --git a/sandbox/CliFrameworkBenchmark/Program.cs b/sandbox/CliFrameworkBenchmark/Program.cs index 131ae0e..bc3a523 100644 --- a/sandbox/CliFrameworkBenchmark/Program.cs +++ b/sandbox/CliFrameworkBenchmark/Program.cs @@ -2,6 +2,7 @@ // https://github.com/Tyrrrz/CliFx/tree/master/CliFx.Benchmarks/ using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Engines; using BenchmarkDotNet.Running; namespace Cocona.Benchmark.External; @@ -10,6 +11,6 @@ class Program { static void Main(string[] args) { - BenchmarkRunner.Run(DefaultConfig.Instance.WithOptions(ConfigOptions.DisableOptimizationsValidator)); + BenchmarkRunner.Run(); } } diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index ab50f8e..c66d0b8 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -4,6 +4,8 @@ using System.Data; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Net.Http; +using System.Net.Http.Headers; using System.Numerics; using System.Reflection; using System.Reflection.Metadata; @@ -29,7 +31,7 @@ builder.Add(); -builder.Run(args); +await builder.RunAsync(args); // var s = "foo"; // s.AsSpan().Split(',',). @@ -64,7 +66,7 @@ static void Tests() public class MyClass { - public void Do() + public void Do(CancellationToken cancellationToken) { Console.Write("yeah"); } @@ -370,7 +372,142 @@ namespace ConsoleAppFramework partial class ConsoleApp { + public struct Builder() + { + private static void RunCommand0(ReadOnlySpan args) + { + if (TryShowHelpOrVersion(args, 0)) return; + + + try + { + for (int i = 0; i < args.Length; i++) + { + + var name = args[i]; + + switch (name) + { + + default: + + ThrowArgumentNameNotFound(name); + break; + } + } + + + var instance = new global::MyClass(); + instance.Do(); + } + + catch (Exception ex) + { + Environment.ExitCode = 1; + if (ex is System.ComponentModel.DataAnnotations.ValidationException) + { + LogError(ex.Message); + } + else + { + LogError(ex.ToString()); + } + } + } + + public void RunCore2(string[] args) + { + switch (args[0]) + { + case "do": + RunWithFilterAsync(new Command0Invoker(args[1..]).BuildFilter()).GetAwaiter().GetResult(); + break; + default: + break; + } + } + + // move to ConsoleApp template? + static async Task RunWithFilterAsync(ConsoleAppFilter invoker) + { + using var posixSignalHandler = PosixSignalHandler.Register(Timeout); + + // in core, remove try-catch...? + try + { + await Task.Run(() => invoker.InvokeAsync(posixSignalHandler.Token).AsTask()).WaitAsync(posixSignalHandler.TimeoutToken); + } + catch (OperationCanceledException ex) when (ex.CancellationToken == posixSignalHandler.Token || ex.CancellationToken == posixSignalHandler.TimeoutToken) + { + Environment.ExitCode = 130; + } + catch (Exception ex) + { + Environment.ExitCode = 1; + if (ex is System.ComponentModel.DataAnnotations.ValidationException) + { + LogError(ex.Message); + } + else + { + LogError(ex.ToString()); + } + } + } + + sealed class Command0Invoker(string[] args) : ConsoleAppFilter(null!) + { + public ConsoleAppFilter BuildFilter() + { + var f3 = new TimestampFilter(this); // and DI. + var f2 = new TimestampFilter(f3); + var f1 = new TimestampFilter(f2); + + return f1; + } + + public override ValueTask InvokeAsync(CancellationToken cancellationToken) + { + RunCommand0(args); // pass: cancellationToken. + return default; + } + } + } } } + +public class FilterContext : IServiceProvider +{ + public long Timestamp { get; set; } + public Guid UserId { get; set; } + + object IServiceProvider.GetService(Type serviceType) + { + if (serviceType == typeof(FilterContext)) return this; + throw new InvalidOperationException("Type is invalid:" + serviceType); + } +} + +public abstract class ConsoleAppFilter(ConsoleAppFilter next) +{ + protected ConsoleAppFilter Next = next; + + public abstract ValueTask InvokeAsync(CancellationToken cancellationToken); +} + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = false)] +public sealed class ConsoleAppFilterAttribute : Attribute + where T : ConsoleAppFilter +{ +} + +public class TimestampFilter(ConsoleAppFilter next) + : ConsoleAppFilter(next) +{ + public override ValueTask InvokeAsync(CancellationToken cancellationToken) + { + return Next.InvokeAsync(cancellationToken); + } +} diff --git a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs index 30b861e..22a03ed 100644 --- a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs @@ -222,11 +222,13 @@ static void ValidateParameter(object? value, ParameterInfo parameter, Validation } [MethodImpl(MethodImplOptions.AggressiveInlining)] - static bool TryShowHelpOrVersion(ReadOnlySpan args) + static bool TryShowHelpOrVersion(ReadOnlySpan args, int parameterCount) { if (args.Length == 0) { - ShowHelp(); // TODO: if no args root command, return false. + if (parameterCount == 0) return false; + + ShowHelp(); return true; } diff --git a/src/ConsoleAppFramework5/Emitter.cs b/src/ConsoleAppFramework5/Emitter.cs index 49028b9..9471db2 100644 --- a/src/ConsoleAppFramework5/Emitter.cs +++ b/src/ConsoleAppFramework5/Emitter.cs @@ -12,6 +12,7 @@ public string EmitRun(Command command, bool isRunAsync, string? methodName = nul var hasCancellationToken = command.Parameters.Any(x => x.IsCancellationToken); var hasArgument = command.Parameters.Any(x => x.IsArgument); var hasValidation = command.Parameters.Any(x => x.HasValidation); + var parsableParameterCount = command.Parameters.Count(x => x.IsParsable); // prepare argument variables -> var prepareArgument = new StringBuilder(); @@ -216,7 +217,7 @@ public string EmitRun(Command command, bool isRunAsync, string? methodName = nul var code = $$""" {{accessibility}} static {{unsafeCode}}{{returnType}} {{methodName}}({{argsType}} args{{commandMethodType}}) { - if (TryShowHelpOrVersion(args)) return; + if (TryShowHelpOrVersion(args, {{parsableParameterCount}})) return; {{prepareArgument}} try diff --git a/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs b/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs index 2f5255d..474b87b 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs @@ -63,7 +63,7 @@ public static (Compilation, ImmutableArray) RunGenerator(string sour return (newCompilation, diagnostics); } - public static string CompileAndExecute(string source, string[] args, string[]? preprocessorSymbols = null, AnalyzerConfigOptionsProvider? options = null) + public static (Compilation, ImmutableArray, string) CompileAndExecute(string source, string[] args, string[]? preprocessorSymbols = null, AnalyzerConfigOptionsProvider? options = null) { var (compilation, diagnostics) = RunGenerator(source, preprocessorSymbols, options); @@ -85,12 +85,12 @@ public static string CompileAndExecute(string source, string[] args, string[]? p Console.SetOut(stringWriter); // load and invoke Main(args) - var loadContext = new AssemblyLoadContext("source-generator", isCollectible: true); + var loadContext = new AssemblyLoadContext("source-generator", isCollectible: true); // isCollectible to support Unload var assembly = loadContext.LoadFromStream(ms); assembly.EntryPoint!.Invoke(null, new object[] { args }); loadContext.Unload(); - return stringWriter.ToString(); + return (compilation, diagnostics, stringWriter.ToString()); } finally { @@ -101,6 +101,8 @@ public static string CompileAndExecute(string source, string[] args, string[]? p public class VerifyHelper(ITestOutputHelper output, string idPrefix) { + // Diagnostics Verify + public void Ok(string code, [CallerArgumentExpression("code")] string? codeExpr = null) { output.WriteLine(codeExpr); @@ -141,6 +143,22 @@ public void Verify(int id, string code, string diagnosticsCodeSpan, [CallerArgum return diagnostics.Select(x => (x.Id, GetLocationText(x))).ToArray(); } + // Execute and check stdout result + + public void Execute(string code, string args, string expected, [CallerArgumentExpression("code")] string? codeExpr = null) + { + output.WriteLine(codeExpr); + + var (compilation, diagnostics, stdout) = CSharpGeneratorRunner.CompileAndExecute(code, args.Split(' ')); + foreach (var item in diagnostics) + { + output.WriteLine(item.ToString()); + } + OutputGeneratedCode(compilation); + + stdout.Should().Be(expected); + } + string GetLocationText(Diagnostic diagnostic) { var location = diagnostic.Location; diff --git a/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppBuilderTest.cs b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppBuilderTest.cs index 76aa84a..966ed12 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppBuilderTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppBuilderTest.cs @@ -1,14 +1,16 @@ -using System; +using Microsoft.VisualStudio.TestPlatform.Utilities; +using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using Xunit.Abstractions; namespace ConsoleAppFramework.GeneratorTests; -public class ConsoleAppBuilderTest +public class ConsoleAppBuilderTest(ITestOutputHelper output) { - static string[] ToArgs(string args) => args.Split(' '); + VerifyHelper verifier = new VerifyHelper(output, "CAF"); [Fact] public void BuilderRun() @@ -22,15 +24,15 @@ public void BuilderRun() builder.Run(args); """; - CSharpGeneratorRunner.CompileAndExecute(code, ToArgs("foo --x 10 --y 20")).Should().Be("30"); - CSharpGeneratorRunner.CompileAndExecute(code, ToArgs("bar --x 20 --y 30")).Should().Be("50"); - CSharpGeneratorRunner.CompileAndExecute(code, ToArgs("bar --x 20")).Should().Be("30"); + verifier.Execute(code, "foo --x 10 --y 20", "30"); + verifier.Execute(code, "bar --x 20 --y 30", "50"); + verifier.Execute(code, "bar --x 20", "30"); Environment.ExitCode.Should().Be(0); - CSharpGeneratorRunner.CompileAndExecute(code, ToArgs("baz --x 40 --y takoyaki")).Should().Be("40takoyaki"); + verifier.Execute(code, "baz --x 40 --y takoyaki", "40takoyaki"); Environment.ExitCode.Should().Be(10); Environment.ExitCode = 0; - CSharpGeneratorRunner.CompileAndExecute(code, ToArgs("boz --x 40")).Should().Be("80"); + verifier.Execute(code, "boz --x 40", "80"); } [Fact] @@ -45,15 +47,15 @@ public void BuilderRunAsync() await builder.RunAsync(args); """; - CSharpGeneratorRunner.CompileAndExecute(code, ToArgs("foo --x 10 --y 20")).Should().Be("30"); - CSharpGeneratorRunner.CompileAndExecute(code, ToArgs("bar --x 20 --y 30")).Should().Be("50"); - CSharpGeneratorRunner.CompileAndExecute(code, ToArgs("bar --x 20")).Should().Be("30"); + verifier.Execute(code, "foo --x 10 --y 20", "30"); + verifier.Execute(code, "bar --x 20 --y 30", "50"); + verifier.Execute(code, "bar --x 20", "30"); Environment.ExitCode.Should().Be(0); - CSharpGeneratorRunner.CompileAndExecute(code, ToArgs("baz --x 40 --y takoyaki")).Should().Be("40takoyaki"); + verifier.Execute(code, "baz --x 40 --y takoyaki", "40takoyaki"); Environment.ExitCode.Should().Be(10); Environment.ExitCode = 0; - CSharpGeneratorRunner.CompileAndExecute(code, ToArgs("boz --x 40")).Should().Be("80"); + verifier.Execute(code, "boz --x 40", "80"); } [Fact] @@ -91,9 +93,114 @@ public static void Sum() } """; - CSharpGeneratorRunner.CompileAndExecute(code, ToArgs("do")).Should().Be("yeah"); + verifier.Execute(code, "do", "yeah"); + verifier.Execute(code, "sum --x 1 --y 2", "3"); + verifier.Execute(code, "echo --msg takoyaki", "takoyaki"); + } + + [Fact] + public void ClassDispose() + { + verifier.Execute(""" +var builder = ConsoleApp.CreateBuilder(); +builder.Add(); +builder.Run(args); + +public class MyClass : IDisposable +{ + public void Do() + { + Console.Write("yeah:"); + } + + public void Dispose() + { + Console.Write("disposed!"); + } +} +""", "do", "yeah:disposed!"); + + verifier.Execute(""" +var builder = ConsoleApp.CreateBuilder(); +builder.Add(); +await builder.RunAsync(args); + +public class MyClass : IDisposable +{ + public void Do() + { + Console.Write("yeah:"); + } + + public void Dispose() + { + Console.Write("disposed!"); + } +} +""", "do", "yeah:disposed!"); + + verifier.Execute(""" +var builder = ConsoleApp.CreateBuilder(); +builder.Add(); +await builder.RunAsync(args); +public class MyClass : IAsyncDisposable +{ + public void Do() + { + Console.Write("yeah:"); + } + + public ValueTask DisposeAsync() + { + Console.Write("disposed!"); + return default; } } +""", "do", "yeah:disposed!"); + } + + [Fact] + public void ClassWithDI() + { + verifier.Execute(""" +var serviceCollection = new MiniDI(); +serviceCollection.Register(typeof(string), "hoge!"); +serviceCollection.Register(typeof(int), 9999); +ConsoleApp.ServiceProvider = serviceCollection; + +var builder = ConsoleApp.CreateBuilder(); +builder.Add(); +builder.Run(args); + +public class MyClass(string foo, int bar) +{ + public void Do() + { + Console.Write("yeah:"); + Console.Write(foo); + Console.Write(bar); + } +} + +public class MiniDI : IServiceProvider +{ + System.Collections.Generic.Dictionary dict = new(); + + public void Register(Type type, object instance) + { + dict[type] = instance; + } + + public object GetService(Type serviceType) + { + return dict.TryGetValue(serviceType, out var instance) ? instance : null; + } +} +""", "do", "yeah:hoge!9999"); + } + +} + diff --git a/tests/ConsoleAppFramework.GeneratorTests/RunTest.cs b/tests/ConsoleAppFramework.GeneratorTests/RunTest.cs index ccd0957..c0d477e 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/RunTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/RunTest.cs @@ -10,37 +10,28 @@ namespace ConsoleAppFramework.GeneratorTests; -public class Test // (ITestOutputHelper output) +public class Test(ITestOutputHelper output) { - static string[] ToArgs(string args) - { - return args.Split(' '); - } + VerifyHelper verifier = new VerifyHelper(output, "CAF"); [Fact] public void SyncRun() { - var result = CSharpGeneratorRunner.CompileAndExecute(""" -ConsoleApp.Run(args, (int x, int y) => { Console.Write((x + y)); }); -""", ToArgs("--x 10 --y 20")); - - result.Should().Be("30"); + verifier.Execute("ConsoleApp.Run(args, (int x, int y) => { Console.Write((x + y)); });", "--x 10 --y 20", "30"); } [Fact] public void ValidateOne() { - var result = CSharpGeneratorRunner.CompileAndExecute(""" -ConsoleApp.Run(args, ([Range(1, 10)]int x, [Range(100, 200)]int y) => { Console.Write((x + y)); }); -""", ToArgs("--x 100 --y 140")); - var expected = """ The field x must be between 1 and 10. """; - result.Should().Be(expected); + verifier.Execute(""" +ConsoleApp.Run(args, ([Range(1, 10)]int x, [Range(100, 200)]int y) => { Console.Write((x + y)); }); +""", "--x 100 --y 140", expected); Environment.ExitCode.Should().Be(1); Environment.ExitCode = 0; @@ -49,10 +40,6 @@ The field x must be between 1 and 10. [Fact] public void ValidateTwo() { - var result = CSharpGeneratorRunner.CompileAndExecute(""" -ConsoleApp.Run(args, ([Range(1, 10)]int x, [Range(100, 200)]int y) => { Console.Write((x + y)); }); -""", ToArgs("--x 100 --y 240")); - var expected = """ The field x must be between 1 and 10. The field y must be between 100 and 200. @@ -60,7 +47,9 @@ The field y must be between 100 and 200. """; - result.Should().Be(expected); + verifier.Execute(""" +ConsoleApp.Run(args, ([Range(1, 10)]int x, [Range(100, 200)]int y) => { Console.Write((x + y)); }); +""", "--x 100 --y 240", expected); Environment.ExitCode.Should().Be(1); Environment.ExitCode = 0; diff --git a/tests/ConsoleAppFramework.Tests/Integration/CommandFilterTest.cs b/tests/ConsoleAppFramework.Tests/Integration/CommandFilterTest.cs index faa8105..07ffc8b 100644 --- a/tests/ConsoleAppFramework.Tests/Integration/CommandFilterTest.cs +++ b/tests/ConsoleAppFramework.Tests/Integration/CommandFilterTest.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; using Microsoft.Extensions.Hosting; @@ -10,34 +11,35 @@ namespace ConsoleAppFramework.Integration.Test; public class FilterTest { - [Fact] - public void ApplyAttributeFilterTest() - { - using var console = new CaptureConsoleOutput(); - var args = new[] { "test-argument-name" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("[in filter] before"); - console.Output.Should().Contain(args[0]); - console.Output.Should().Contain("[in filter] after"); - } + [Fact] + public void ApplyAttributeFilterTest() + { + using var console = new CaptureConsoleOutput(); + var args = new[] { "test-argument-name" }; + Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); + console.Output.Should().Contain("[in filter] before"); + console.Output.Should().Contain(args[0]); + console.Output.Should().Contain("[in filter] after"); + } - /// - private class TestConsoleApp : ConsoleAppBase - { - [RootCommand] - [ConsoleAppFilter(typeof(TestFilter))] - public void RootCommand([Option(index: 0)]string someArgument) => Console.WriteLine(someArgument); - } + /// + private class TestConsoleApp : ConsoleAppBase + { + [RootCommand] + [ConsoleAppFilter(typeof(TestFilter))] + public void RootCommand([Option(index: 0)] string someArgument) => Console.WriteLine(someArgument); + } + + /// + private class TestFilter : ConsoleAppFilter + { + /// + public override async ValueTask Invoke(ConsoleAppContext context, Func next) + { + Console.WriteLine("[in filter] before"); + await next(context); + Console.WriteLine("[in filter] after"); + } + } +} - /// - private class TestFilter : ConsoleAppFilter - { - /// - public override async ValueTask Invoke(ConsoleAppContext context, Func next) - { - Console.WriteLine("[in filter] before"); - await next(context); - Console.WriteLine("[in filter] after"); - } - } -} \ No newline at end of file From 4c46247fee5b101983efc21e94fe4fadd4c246a6 Mon Sep 17 00:00:00 2001 From: neuecc Date: Wed, 22 May 2024 19:35:48 +0900 Subject: [PATCH 21/54] doing --- sandbox/CliFrameworkBenchmark/Benchmark.cs | 5 +- sandbox/CliFrameworkBenchmark/Program.cs | 5 +- sandbox/GeneratorSandbox/Program.cs | 41 ++++- .../ConsoleAppGenerator.cs | 28 +++- .../DiagnosticDescriptors.cs | 12 +- src/ConsoleAppFramework5/Parser.cs | 80 ++++++---- .../CSharpGeneratorRunner.cs | 18 +-- .../ConsoleAppBuilderTest.cs | 20 +++ .../DiagnosticsTest.cs | 142 +++++++++++++++++- 9 files changed, 293 insertions(+), 58 deletions(-) diff --git a/sandbox/CliFrameworkBenchmark/Benchmark.cs b/sandbox/CliFrameworkBenchmark/Benchmark.cs index b0d4b6a..7eac03f 100644 --- a/sandbox/CliFrameworkBenchmark/Benchmark.cs +++ b/sandbox/CliFrameworkBenchmark/Benchmark.cs @@ -17,14 +17,13 @@ namespace Cocona.Benchmark.External; // use ColdStart strategy to measure startup time evaluation [SimpleJob(RunStrategy.ColdStart, launchCount: 1, warmupCount: 0, iterationCount: 1, invocationCount: 1)] -[RankColumn] [MemoryDiagnoser] [Orderer(SummaryOrderPolicy.FastestToSlowest)] public class Benchmark { private static readonly string[] Arguments = { "--str", "hello world", "-i", "13", "-b" }; - [Benchmark(Description = "Cocona.Lite", Baseline = true)] + [Benchmark(Description = "Cocona.Lite")] public void ExecuteWithCoconaLite() { Cocona.CoconaLiteApp.Run(Arguments); @@ -77,7 +76,7 @@ public int ExecuteWithSystemCommandLine() // ConsoleApp.Run(Arguments, ConsoleAppFrameworkCommand.Execute); //} - [Benchmark(Description = "ConsoleAppFramework v5")] + [Benchmark(Description = "ConsoleAppFramework v5", Baseline = true)] public unsafe void ExecuteConsoleAppFramework() { ConsoleApp.Run(Arguments, &ConsoleAppFrameworkCommand.Execute); diff --git a/sandbox/CliFrameworkBenchmark/Program.cs b/sandbox/CliFrameworkBenchmark/Program.cs index bc3a523..f71dd38 100644 --- a/sandbox/CliFrameworkBenchmark/Program.cs +++ b/sandbox/CliFrameworkBenchmark/Program.cs @@ -2,8 +2,9 @@ // https://github.com/Tyrrrz/CliFx/tree/master/CliFx.Benchmarks/ using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Engines; +using BenchmarkDotNet.Reports; using BenchmarkDotNet.Running; +using Perfolizer.Horology; namespace Cocona.Benchmark.External; @@ -11,6 +12,6 @@ class Program { static void Main(string[] args) { - BenchmarkRunner.Run(); + BenchmarkRunner.Run(DefaultConfig.Instance.WithSummaryStyle(SummaryStyle.Default.WithTimeUnit(TimeUnit.Millisecond))); } } diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index c66d0b8..77bdb93 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -28,7 +28,9 @@ var builder = ConsoleApp.CreateBuilder(); + builder.Add(); +// builder.Add("foo", string (int x, int y) => { return "foo"; }); await builder.RunAsync(args); @@ -376,7 +378,7 @@ public struct Builder() { private static void RunCommand0(ReadOnlySpan args) { - if (TryShowHelpOrVersion(args, 0)) return; + // if (TryShowHelpOrVersion(args, 0)) return; try @@ -398,7 +400,7 @@ private static void RunCommand0(ReadOnlySpan args) var instance = new global::MyClass(); - instance.Do(); + // instance.Do(); } catch (Exception ex) @@ -511,3 +513,38 @@ public override ValueTask InvokeAsync(CancellationToken cancellationToken) return Next.InvokeAsync(cancellationToken); } } + + + + + +public class MyContext : IServiceProvider +{ + public long Timestamp { get; set; } + public Guid UserId { get; set; } + + object IServiceProvider.GetService(Type serviceType) + { + if (serviceType == typeof(MyContext)) return this; + throw new InvalidOperationException("Type is invalid:" + serviceType); + } +} + +public class MyClass23 +{ + public void Do() + { + Console.Write("yeah:"); + } +} + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] +internal sealed class CommandAttribute : Attribute +{ + public string Command { get; } + + public CommandAttribute(string command) + { + this.Command = command; + } +} \ No newline at end of file diff --git a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs index 22a03ed..6e59bc5 100644 --- a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs @@ -102,6 +102,17 @@ 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; + } +} + internal static partial class ConsoleApp { public static IServiceProvider? ServiceProvider { get; set; } @@ -429,6 +440,20 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex var wellKnownTypes = new WellKnownTypes(model.Compilation); + // validation, invoke in loop is not allowed. + foreach (var item in generatorSyntaxContexts) + { + if (item.Name is "Run" or "RunAsync") continue; + foreach (var n in item.Node.Ancestors()) + { + if (n.Kind() is SyntaxKind.WhileStatement or SyntaxKind.DoStatement or SyntaxKind.ForStatement or SyntaxKind.ForEachStatement) + { + sourceProductionContext.ReportDiagnostic(DiagnosticDescriptors.AddInLoopIsNotAllowed, item.Node.GetLocation()); + return; + } + } + } + var group1 = generatorSyntaxContexts.ToLookup(x => { if (x.Name == "Add" && ((x.Node.Expression as MemberAccessExpressionSyntax)?.Name.IsKind(SyntaxKind.GenericName) ?? false)) @@ -454,7 +479,8 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex } return command; - }); + }) + .ToArray(); // evaluate first. var commands2 = group1["Add"] .SelectMany(x => diff --git a/src/ConsoleAppFramework5/DiagnosticDescriptors.cs b/src/ConsoleAppFramework5/DiagnosticDescriptors.cs index aafc26b..548f2ec 100644 --- a/src/ConsoleAppFramework5/DiagnosticDescriptors.cs +++ b/src/ConsoleAppFramework5/DiagnosticDescriptors.cs @@ -34,13 +34,13 @@ public static DiagnosticDescriptor Create(int id, string title, string messageFo public static DiagnosticDescriptor ReturnTypeLambda { get; } = Create( 2, - "Run lambda expressions return type must be void or int or async Task or async Task.", - "Run lambda expressions return type must be void or int or async Task or async Task but returned '{0}'."); + "Command lambda expressions return type must be void or int or async Task or async Task.", + "Command lambda expressions return type must be void or int or async Task or async Task but returned '{0}'."); public static DiagnosticDescriptor ReturnTypeMethod { get; } = Create( 3, - "Run referenced method return type must be void or int or async Task or async Task.", - "Run referenced method return type must be void or int or async Task or async Task but returned '{0}'."); + "Command method return type must be void or int or async Task or async Task.", + "Command method return type must be void or int or async Task or async Task but returned '{0}'."); public static DiagnosticDescriptor SequentialArgument { get; } = Create( 4, @@ -58,4 +58,8 @@ public static DiagnosticDescriptor Create(int id, string title, string messageFo 7, "Command name is duplicated.", "Command name '{0}' is duplicated."); + + public static DiagnosticDescriptor AddInLoopIsNotAllowed { get; } = Create( + 8, + "ConsoleAppBuilder.Add/AddFilter is not allowed in loop statement(while, do, for, foreach)."); } diff --git a/src/ConsoleAppFramework5/Parser.cs b/src/ConsoleAppFramework5/Parser.cs index 2b1cdef..21b334b 100644 --- a/src/ConsoleAppFramework5/Parser.cs +++ b/src/ConsoleAppFramework5/Parser.cs @@ -105,8 +105,19 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta return publicMethods .Select(x => { - // TODO: commandName convert to snake-case - var command = ParseFromMethodSymbol(x, false, x.Name.ToLowerInvariant()); + string commandName; + var commandAttribute = x.GetAttributes().FirstOrDefault(x => x.AttributeClass?.Name == "CommandAttribute"); + if (commandAttribute != null) + { + commandName = (x.GetAttributes()[0].ConstructorArguments[0].Value as string)!; + } + else + { + // TODO: commandName convert to snake-case + commandName = x.Name.ToLowerInvariant(); + } + + var command = ParseFromMethodSymbol(x, false, commandName); if (command == null) return null; command.CommandMethodInfo = methodInfoBase with { MethodName = x.Name }; @@ -249,30 +260,30 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta }); var isFromServices = x.AttributeLists.SelectMany(x => x.Attributes) - .Any(x => - { - var name = x.Name; - if (x.Name is QualifiedNameSyntax qns) - { - name = qns.Right; - } - - var identifier = name.ToString(); - return identifier is "FromServices" or "FromServicesAttribute"; - }); + .Any(x => + { + var name = x.Name; + if (x.Name is QualifiedNameSyntax qns) + { + name = qns.Right; + } + + var identifier = name.ToString(); + return identifier is "FromServices" or "FromServicesAttribute"; + }); var hasArgument = x.AttributeLists.SelectMany(x => x.Attributes) - .Any(x => - { - var name = x.Name; - if (x.Name is QualifiedNameSyntax qns) - { - name = qns.Right; - } - - var identifier = name.ToString(); - return identifier is "Argument" or "ArgumentAttribute"; - }); + .Any(x => + { + var name = x.Name; + if (x.Name is QualifiedNameSyntax qns) + { + name = qns.Right; + } + + var identifier = name.ToString(); + return identifier is "Argument" or "ArgumentAttribute"; + }); var isCancellationToken = SymbolEqualityComparer.Default.Equals(type.Type!, wellKnownTypes.CancellationToken); @@ -362,14 +373,29 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta } else { - // TODO: Arguments[1] is dangerous... - context.ReportDiagnostic(DiagnosticDescriptors.ReturnTypeMethod, node.ArgumentList.Arguments[1].GetLocation(), methodSymbol.ReturnType); + var syntax = methodSymbol.DeclaringSyntaxReferences[0].GetSyntax(); + var location = syntax switch + { + MethodDeclarationSyntax x => x.ReturnType.GetLocation(), + LocalFunctionStatementSyntax x => x.ReturnType.GetLocation(), + _ => node.GetLocation() + }; + + context.ReportDiagnostic(DiagnosticDescriptors.ReturnTypeMethod, location, methodSymbol.ReturnType); return null; } } else { - context.ReportDiagnostic(DiagnosticDescriptors.ReturnTypeMethod, node.ArgumentList.Arguments[1].GetLocation(), methodSymbol.ReturnType); + var syntax = methodSymbol.DeclaringSyntaxReferences[0].GetSyntax(); + var location = syntax switch + { + MethodDeclarationSyntax x => x.ReturnType.GetLocation(), + LocalFunctionStatementSyntax x => x.ReturnType.GetLocation(), + _ => node.GetLocation() + }; + + context.ReportDiagnostic(DiagnosticDescriptors.ReturnTypeMethod, location, methodSymbol.ReturnType); return null; } diff --git a/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs b/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs index 474b87b..93cac87 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs @@ -14,20 +14,6 @@ public static class CSharpGeneratorRunner [ModuleInitializer] public static void InitializeCompilation() { - // running .NET Core system assemblies dir path - var baseAssemblyPath = Path.GetDirectoryName(typeof(object).Assembly.Location)!; - var systemAssemblies = Directory.GetFiles(baseAssemblyPath) - .Where(x => - { - var fileName = Path.GetFileName(x); - if (fileName.EndsWith("Native.dll")) return false; - return fileName.StartsWith("System") || (fileName is "mscorlib.dll" or "netstandard.dll"); - }); - - var references = systemAssemblies - .Select(x => MetadataReference.CreateFromFile(x)) - .ToArray(); - var globalUsings = """ global using System; global using System.Threading.Tasks; @@ -35,6 +21,10 @@ public static void InitializeCompilation() global using ConsoleAppFramework; """; + var references = AppDomain.CurrentDomain.GetAssemblies() + .Where(x => !x.IsDynamic && !string.IsNullOrWhiteSpace(x.Location)) + .Select(x => MetadataReference.CreateFromFile(x.Location)); + var compilation = CSharpCompilation.Create("generatortest", references: references, syntaxTrees: [CSharpSyntaxTree.ParseText(globalUsings)], diff --git a/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppBuilderTest.cs b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppBuilderTest.cs index 966ed12..084b3b0 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppBuilderTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppBuilderTest.cs @@ -200,6 +200,26 @@ public object GetService(Type serviceType) """, "do", "yeah:hoge!9999"); } + [Fact] + public void Command() + { + var code = """ +var builder = ConsoleApp.CreateBuilder(); +builder.Add(); +builder.Run(args); + +public class MyClass() +{ + [Command("nomunomu")] + public void Do() + { + Console.Write("yeah"); + } +} +"""; + + verifier.Execute(code, "nomunomu", "yeah"); + } } diff --git a/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs b/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs index d9b4e4f..5ca180f 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs @@ -34,8 +34,8 @@ public void InvalidReturnTypeFromLambda() [Fact] public void InvalidReturnTypeFromMethodReference() { - verifier.Verify(3, "ConsoleApp.Run(args, Invoke); float Invoke(int x, int y) => 0.3f;", "Invoke"); - verifier.Verify(3, "ConsoleApp.Run(args, InvokeAsync); async Task InvokeAsync(int x, int y) => 0.3f;", "InvokeAsync"); + verifier.Verify(3, "ConsoleApp.Run(args, Invoke); float Invoke(int x, int y) => 0.3f;", "float"); + verifier.Verify(3, "ConsoleApp.Run(args, InvokeAsync); async Task InvokeAsync(int x, int y) => 0.3f;", "Task"); verifier.Ok("ConsoleApp.Run(args, Run); void Run(int x, int y) { };"); verifier.Ok("ConsoleApp.Run(args, Run); static void Run(int x, int y) { };"); verifier.Ok("ConsoleApp.Run(args, Run); int Run(int x, int y) => -1;"); @@ -59,8 +59,8 @@ public void RunAsyncValidation() verifier.Ok("ConsoleApp.RunAsync(args, async Task (int x, int y) => { })"); verifier.Ok("ConsoleApp.RunAsync(args, async Task (int x, int y) => { })"); - verifier.Verify(3, "ConsoleApp.RunAsync(args, Invoke); float Invoke(int x, int y) => 0.3f;", "Invoke"); - verifier.Verify(3, "ConsoleApp.RunAsync(args, InvokeAsync); async Task InvokeAsync(int x, int y) => 0.3f;", "InvokeAsync"); + verifier.Verify(3, "ConsoleApp.RunAsync(args, Invoke); float Invoke(int x, int y) => 0.3f;", "float"); + verifier.Verify(3, "ConsoleApp.RunAsync(args, InvokeAsync); async Task InvokeAsync(int x, int y) => 0.3f;", "Task"); verifier.Ok("ConsoleApp.RunAsync(args, Run); void Run(int x, int y) { };"); verifier.Ok("ConsoleApp.RunAsync(args, Run); static void Run(int x, int y) { };"); verifier.Ok("ConsoleApp.RunAsync(args, Run); int Run(int x, int y) => -1;"); @@ -90,7 +90,7 @@ public void FunctionPointerValidation() [Fact] public void BuilderAddConstCommandName() { - verifier.Verify(6,""" + verifier.Verify(6, """ var builder = ConsoleApp.CreateBuilder(); var baz = "foo"; builder.Add(baz, (int x, int y) => { } ); @@ -112,4 +112,136 @@ public void DuplicateCommandName() builder.Add("foo", (int x, int y) => { } ); """, "\"foo\""); } + + [Fact] + public void DuplicateCommandNameClass() + { + verifier.Verify(7, """ +var builder = ConsoleApp.CreateBuilder(); +builder.Add(); + +public class MyClass +{ + public void Do() + { + Console.Write("yeah:"); + } + + public void Do(int i) + { + Console.Write("yeah:"); + } +} +""", "builder.Add()"); + + verifier.Verify(7, """ +var builder = ConsoleApp.CreateBuilder(); +builder.Add("do", (int x, int y) => { } ); +builder.Add(); +builder.Run(args); + +public class MyClass +{ + public void Do() + { + Console.Write("yeah:"); + } +} +""", "builder.Add()"); + } + + [Fact] + public void AddInLoop() + { + var myClass = """ +public class MyClass +{ + public void Do() + { + Console.Write("yeah:"); + } +} +"""; + verifier.Verify(8, $$""" +var builder = ConsoleApp.CreateBuilder(); +while (true) +{ + builder.Add(); +} + +{{myClass}} +""", "builder.Add()"); + + verifier.Verify(8, $$""" +var builder = ConsoleApp.CreateBuilder(); +for (int i = 0; i < 10; i++) +{ + builder.Add(); +} + +{{myClass}} +""", "builder.Add()"); + + verifier.Verify(8, $$""" +var builder = ConsoleApp.CreateBuilder(); +do +{ + builder.Add(); +} while(true); + +{{myClass}} +""", "builder.Add()"); + + verifier.Verify(8, $$""" +var builder = ConsoleApp.CreateBuilder(); +foreach (var item in new[]{1,2,3}) +{ + builder.Add(); +} + +{{myClass}} +""", "builder.Add()"); + } + + [Fact] + public void ErrorInBuilderAPI() + { + verifier.Verify(3, $$""" +var builder = ConsoleApp.CreateBuilder(); +builder.Add(); + +public class MyClass +{ + public string Do() + { + Console.Write("yeah:"); + return "foo"; + } +} +""", "string"); + + verifier.Verify(3, $$""" +var builder = ConsoleApp.CreateBuilder(); +builder.Add(); + +public class MyClass +{ + public async Task Do() + { + Console.Write("yeah:"); + return "foo"; + } +} +""", "Task"); + + verifier.Verify(2, $$""" +var builder = ConsoleApp.CreateBuilder(); +builder.Add("foo", string (int x, int y) => { return "foo"; }); +""", "string"); + + verifier.Verify(2, $$""" +var builder = ConsoleApp.CreateBuilder(); +builder.Add("foo", async Task (int x, int y) => { return "foo"; }); +""", "Task"); + } } From 9575c2f97055182f33aa58c1a7cf26434a5cdba6 Mon Sep 17 00:00:00 2001 From: neuecc Date: Thu, 23 May 2024 03:00:11 +0900 Subject: [PATCH 22/54] commandPath(WIP) --- sandbox/GeneratorSandbox/Program.cs | 13 +++++++- src/ConsoleAppFramework5/Command.cs | 4 ++- .../ConsoleAppGenerator.cs | 8 ++--- src/ConsoleAppFramework5/Emitter.cs | 30 +++++++++++++++---- src/ConsoleAppFramework5/Parser.cs | 20 +++++++++---- 5 files changed, 59 insertions(+), 16 deletions(-) diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index 77bdb93..10cbc3a 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -30,7 +30,7 @@ var builder = ConsoleApp.CreateBuilder(); builder.Add(); -// builder.Add("foo", string (int x, int y) => { return "foo"; }); +builder.Add("foo/tako", (int x, int y) => { return "foo"; }); await builder.RunAsync(args); @@ -424,6 +424,17 @@ public void RunCore2(string[] args) case "do": RunWithFilterAsync(new Command0Invoker(args[1..]).BuildFilter()).GetAwaiter().GetResult(); break; + case "tako": + switch (args[1]) // incr... + { + case "foo": + break; + default: + break; + } + break; + //case "tako": + //break; default: break; } diff --git a/src/ConsoleAppFramework5/Command.cs b/src/ConsoleAppFramework5/Command.cs index 985ee56..6a9d3d5 100644 --- a/src/ConsoleAppFramework5/Command.cs +++ b/src/ConsoleAppFramework5/Command.cs @@ -23,8 +23,10 @@ public record class Command { public required bool IsAsync { get; init; } // Task or Task public required bool IsVoid { get; init; } // void or int + public string CommandFullName => (CommandPath.Length == 0) ? CommandName : $"{string.Join("/", CommandPath)}/{CommandName}"; + public bool IsRootCommand => CommandFullName == ""; + public required string[] CommandPath { get; init; } public required string CommandName { get; init; } - public bool IsRootCommand => CommandName == ""; public required CommandParameter[] Parameters { get; init; } public required string Description { get; init; } public required MethodKind MethodKind { get; init; } diff --git a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs index 6e59bc5..8a520b0 100644 --- a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs @@ -472,9 +472,9 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex var command = parser.ParseAndValidateForCommand(); // validation command name duplicate - if (command != null && !names.Add(command.CommandName)) + if (command != null && !names.Add(command.CommandFullName)) { - sourceProductionContext.ReportDiagnostic(DiagnosticDescriptors.DuplicateCommandName, x.Node.ArgumentList.Arguments[0].GetLocation(), command!.CommandName); + sourceProductionContext.ReportDiagnostic(DiagnosticDescriptors.DuplicateCommandName, x.Node.ArgumentList.Arguments[0].GetLocation(), command!.CommandFullName); return null; } @@ -491,9 +491,9 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex // validation command name duplicate? foreach (var command in commands) { - if (command != null && !names.Add(command.CommandName)) + if (command != null && !names.Add(command.CommandFullName)) { - sourceProductionContext.ReportDiagnostic(DiagnosticDescriptors.DuplicateCommandName, x.Node.GetLocation(), command!.CommandName); + sourceProductionContext.ReportDiagnostic(DiagnosticDescriptors.DuplicateCommandName, x.Node.GetLocation(), command!.CommandFullName); return [null]; } } diff --git a/src/ConsoleAppFramework5/Emitter.cs b/src/ConsoleAppFramework5/Emitter.cs index 9471db2..400c6e2 100644 --- a/src/ConsoleAppFramework5/Emitter.cs +++ b/src/ConsoleAppFramework5/Emitter.cs @@ -287,27 +287,47 @@ public string EmitBuilder(Command[] commands, bool emitSync, bool emitAsync) var fieldType = command.BuildDelegateSignature(out _); // for builder, always generate Action/Func. fields.AppendLine($" {fieldType} command{i} = default!;"); - addCase.AppendLine($" case \"{command.CommandName}\":"); + addCase.AppendLine($" case \"{command.CommandFullName}\":"); addCase.AppendLine($" this.command{i} = Unsafe.As<{fieldType}>(command);"); addCase.AppendLine($" break;"); commandArgs = $", command{i}"; } + // TODO: case grouping + var runIndent = " "; + for (int j = 0; j < command.CommandPath.Length; j++) + { + var path = command.CommandPath[j]; + var subCommand = $$""" +{{runIndent}}switch (args[{{j + 1}}]) +{{runIndent}}{ +{{runIndent}} case "{{path}}": +{{runIndent}} break; +{{runIndent}}} +{{runIndent}}break; +case "": +"""; + runIndent += " "; + } + + if (emitSync) { - runCase.AppendLine($" case \"{command.CommandName}\":"); + runCommands.AppendLine(EmitRun(command, false, $"RunCommand{i}")); + + runCase.AppendLine($" case \"{command.CommandFullName}\":"); runCase.AppendLine($" RunCommand{i}(args.AsSpan(1){commandArgs});"); runCase.AppendLine($" break;"); - runCommands.AppendLine(EmitRun(command, false, $"RunCommand{i}")); } if (emitAsync) { - runAsyncCase.AppendLine($" case \"{command.CommandName}\":"); + runAsyncCommands.AppendLine(EmitRun(command, true, $"RunAsyncCommand{i}")); + + runAsyncCase.AppendLine($" case \"{command.CommandFullName}\":"); runAsyncCase.AppendLine($" result = RunAsyncCommand{i}(args[1..]{commandArgs});"); runAsyncCase.AppendLine($" break;"); - runAsyncCommands.AppendLine(EmitRun(command, true, $"RunAsyncCommand{i}")); } } diff --git a/src/ConsoleAppFramework5/Parser.cs b/src/ConsoleAppFramework5/Parser.cs index 21b334b..b2da7ef 100644 --- a/src/ConsoleAppFramework5/Parser.cs +++ b/src/ConsoleAppFramework5/Parser.cs @@ -1,6 +1,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Linq; using System.Reflection.Metadata; namespace ConsoleAppFramework; @@ -12,7 +13,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta var args = node.ArgumentList.Arguments; if (args.Count == 2) // 0 = args, 1 = lambda { - var command = ExpressionToCommand(args[1].Expression, ""); // rootCommand = commandName = "" + var command = ExpressionToCommand(args[1].Expression, [], ""); // rootCommand(path and commandName = "") if (command != null) { return ValidateCommand(command); @@ -38,8 +39,15 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta return null; } + string[] path = []; var name = (commandName.Expression as LiteralExpressionSyntax)!.Token.ValueText; - var command = ExpressionToCommand(args[1].Expression, name); + var pathAndName = name.Split(['/'], StringSplitOptions.RemoveEmptyEntries); + if (pathAndName.Length > 1) + { + path = pathAndName.AsSpan(0, pathAndName.Length - 1).ToArray(); + name = pathAndName[^1]; + } + var command = ExpressionToCommand(args[1].Expression, path, name); if (command != null) { return ValidateCommand(command); @@ -126,7 +134,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta .ToArray(); } - Command? ExpressionToCommand(ExpressionSyntax expression, string commandName) + Command? ExpressionToCommand(ExpressionSyntax expression, string[] commandPath, string commandName) { var lambda = expression as ParenthesizedLambdaExpressionSyntax; if (lambda == null) @@ -152,13 +160,13 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta } else { - return ParseFromLambda(lambda, commandName); + return ParseFromLambda(lambda, commandPath, commandName); } return null; } - Command? ParseFromLambda(ParenthesizedLambdaExpressionSyntax lambda, string commandName) + Command? ParseFromLambda(ParenthesizedLambdaExpressionSyntax lambda, string[] commandPath, string commandName) { var isAsync = lambda.AsyncKeyword.IsKind(SyntaxKind.AsyncKeyword); @@ -325,6 +333,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta var cmd = new Command { CommandName = commandName, + CommandPath = commandPath, IsAsync = isAsync, IsVoid = isVoid, Parameters = parameters, @@ -453,6 +462,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta var cmd = new Command { CommandName = commandName, + CommandPath = [], IsAsync = isAsync, IsVoid = isVoid, Parameters = parameters, From 5c88eac9aaf9b4af39634429d37d8ef58b1b8d37 Mon Sep 17 00:00:00 2001 From: neuecc Date: Thu, 23 May 2024 17:03:49 +0900 Subject: [PATCH 23/54] WIP --- sandbox/GeneratorSandbox/Program.cs | 19 ++++- src/ConsoleAppFramework5/Emitter.cs | 71 ++++++++++++++----- .../IndentStringBuilder.cs | 32 +++++++++ 3 files changed, 103 insertions(+), 19 deletions(-) create mode 100644 tests/ConsoleAppFramework.GeneratorTests/IndentStringBuilder.cs diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index 10cbc3a..2e214ff 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -434,7 +434,7 @@ public void RunCore2(string[] args) } break; //case "tako": - //break; + //break; default: break; } @@ -526,6 +526,23 @@ public override ValueTask InvokeAsync(CancellationToken cancellationToken) } +public class LogExecutionTimeFilter(ConsoleAppFilter next) + : ConsoleAppFilter(next) +{ + public override async ValueTask InvokeAsync(CancellationToken cancellationToken) + { + var startingTime = Stopwatch.GetTimestamp(); + try + { + await Next.InvokeAsync(cancellationToken); + } + finally + { + var elapsed = Stopwatch.GetElapsedTime(startingTime); + ConsoleApp.Log($"Execution Time: {elapsed.ToString()}"); + } + } +} diff --git a/src/ConsoleAppFramework5/Emitter.cs b/src/ConsoleAppFramework5/Emitter.cs index 400c6e2..6f44a97 100644 --- a/src/ConsoleAppFramework5/Emitter.cs +++ b/src/ConsoleAppFramework5/Emitter.cs @@ -276,6 +276,59 @@ public string EmitBuilder(Command[] commands, bool emitSync, bool emitAsync) var runAsyncCommands = new StringBuilder(); var runCase = new StringBuilder(); var runAsyncCase = new StringBuilder(); + var ids = commands.Select((x, i) => (x, i)).ToDictionary(x => x.x, x => x.i); + + void EmitBody(IEnumerable> groupedCommands, int depth) + { + // TODO: emitAsync + if (emitSync) + { + runCase.AppendLine($" switch (args[{depth}])"); + runCase.AppendLine($" {{"); + + var implDefault = false; + foreach (var commands in groupedCommands) + { + if (commands.Key == null) implDefault = true; + + // runCase.AppendLine($" case:{commands.Key}"); + var key = commands.Key == null ? "default:" : $"case {commands.Key}:"; + runCase.AppendLine($" {key}"); + + if (implDefault) + { + var command = commands.First(); // duplicate name is not allowed so always single command + var id = ids[command]; + string commandArgs = ""; + if (command.DelegateBuildType != DelegateBuildType.None) + { + commandArgs = $", command{id}"; + } + runCase.AppendLine($" RunCommand{id}(args.AsSpan({depth + 1}){commandArgs});"); + } + else + { + var nextDepth = depth + 1; + var nextGroup = commands.GroupBy(x => (x.CommandPath.Length < nextDepth) ? x.CommandName : x.CommandPath[nextDepth]); + EmitBody(nextGroup, nextDepth); // rec + } + runCase.AppendLine($" break;"); + runCase.AppendLine($" }}"); + } + + if (!implDefault) + { + runCase.AppendLine($" default:"); + runCase.AppendLine($" break;"); + } + runCase.AppendLine($" }}"); + } + } + + // TODO:WIP + // EmitBody(commands.GroupBy(x => x.CommandPath.Length == 0 ? x.CommandName : x.CommandPath[0]), 0); + + for (int i = 0; i < commands.Length; i++) { @@ -294,24 +347,6 @@ public string EmitBuilder(Command[] commands, bool emitSync, bool emitAsync) commandArgs = $", command{i}"; } - // TODO: case grouping - var runIndent = " "; - for (int j = 0; j < command.CommandPath.Length; j++) - { - var path = command.CommandPath[j]; - var subCommand = $$""" -{{runIndent}}switch (args[{{j + 1}}]) -{{runIndent}}{ -{{runIndent}} case "{{path}}": -{{runIndent}} break; -{{runIndent}}} -{{runIndent}}break; -case "": -"""; - runIndent += " "; - } - - if (emitSync) { runCommands.AppendLine(EmitRun(command, false, $"RunCommand{i}")); diff --git a/tests/ConsoleAppFramework.GeneratorTests/IndentStringBuilder.cs b/tests/ConsoleAppFramework.GeneratorTests/IndentStringBuilder.cs new file mode 100644 index 0000000..1e4515b --- /dev/null +++ b/tests/ConsoleAppFramework.GeneratorTests/IndentStringBuilder.cs @@ -0,0 +1,32 @@ +using System.Text; + +namespace ConsoleAppFramework.GeneratorTests; + +public class IndentStringBuilder(int level) +{ + StringBuilder builder = new StringBuilder(); + + public void Indent() + { + level++; + } + + public void Unindent() + { + level--; + } + + public void AppendLine(string text) + { + if (level != 0) + { + builder.Append(' ', 4 * level); + } + builder.AppendLine(text); + } + + public override string ToString() + { + return builder.ToString(); + } +} From 230433bc75ca41a69a582107bf3f7ee090f1a8c7 Mon Sep 17 00:00:00 2001 From: neuecc Date: Thu, 23 May 2024 17:17:17 +0900 Subject: [PATCH 24/54] TODO:IndentStringBuilder --- src/ConsoleAppFramework5/Emitter.cs | 26 +++++----- .../IndentStringBuilder.cs | 51 +++++++++++++++++++ 2 files changed, 64 insertions(+), 13 deletions(-) create mode 100644 src/ConsoleAppFramework5/IndentStringBuilder.cs diff --git a/src/ConsoleAppFramework5/Emitter.cs b/src/ConsoleAppFramework5/Emitter.cs index 6f44a97..7c0b7bb 100644 --- a/src/ConsoleAppFramework5/Emitter.cs +++ b/src/ConsoleAppFramework5/Emitter.cs @@ -15,10 +15,10 @@ public string EmitRun(Command command, bool isRunAsync, string? methodName = nul var parsableParameterCount = command.Parameters.Count(x => x.IsParsable); // prepare argument variables -> - var prepareArgument = new StringBuilder(); + var prepareArgument = new IndentStringBuilder(2); if (hasCancellationToken) { - prepareArgument.AppendLine(" using var posixSignalHandler = PosixSignalHandler.Register(Timeout);"); + prepareArgument.AppendLine("using var posixSignalHandler = PosixSignalHandler.Register(Timeout);"); } for (var i = 0; i < command.Parameters.Length; i++) { @@ -26,39 +26,39 @@ public string EmitRun(Command command, bool isRunAsync, string? methodName = nul if (parameter.IsParsable) { var defaultValue = parameter.HasDefaultValue ? parameter.DefaultValueToString() : $"default({parameter.Type.ToFullyQualifiedFormatDisplayString()})"; - prepareArgument.AppendLine($" var arg{i} = {defaultValue};"); + prepareArgument.AppendLine($"var arg{i} = {defaultValue};"); if (!parameter.HasDefaultValue) { - prepareArgument.AppendLine($" var arg{i}Parsed = false;"); + prepareArgument.AppendLine($"var arg{i}Parsed = false;"); } } else if (parameter.IsCancellationToken) { - prepareArgument.AppendLine($" var arg{i} = posixSignalHandler.Token;"); + prepareArgument.AppendLine($"var arg{i} = posixSignalHandler.Token;"); } else if (parameter.IsFromServices) { var type = parameter.Type.ToFullyQualifiedFormatDisplayString(); - prepareArgument.AppendLine($" var arg{i} = ({type})ServiceProvider!.GetService(typeof({type}))!;"); + prepareArgument.AppendLine($"var arg{i} = ({type})ServiceProvider!.GetService(typeof({type}))!;"); } } // parse indexed argument([Argument] parameter) - var indexedArgument = new StringBuilder(); + var indexedArgument = new IndentStringBuilder(4); for (int i = 0; i < command.Parameters.Length; i++) { var parameter = command.Parameters[i]; if (!parameter.IsArgument) continue; - indexedArgument.AppendLine($" if (i == {parameter.ArgumentIndex})"); - indexedArgument.AppendLine(" {"); - indexedArgument.AppendLine($" {parameter.BuildParseMethod(i, parameter.Name, wellKnownTypes, increment: false)}"); + indexedArgument.AppendLine($"if (i == {parameter.ArgumentIndex})"); + indexedArgument.AppendLine("{"); + indexedArgument.IndentAppendLine($"{parameter.BuildParseMethod(i, parameter.Name, wellKnownTypes, increment: false)}"); if (!parameter.HasDefaultValue) { - indexedArgument.AppendLine($" arg{i}Parsed = true;"); + indexedArgument.AppendLine($"arg{i}Parsed = true;"); } - indexedArgument.AppendLine(" continue;"); - indexedArgument.AppendLine(" }"); + indexedArgument.AppendLine("continue;"); + indexedArgument.UnindentAppendLine("}"); } // parse argument(fast, switch directly) -> diff --git a/src/ConsoleAppFramework5/IndentStringBuilder.cs b/src/ConsoleAppFramework5/IndentStringBuilder.cs new file mode 100644 index 0000000..5bbbd46 --- /dev/null +++ b/src/ConsoleAppFramework5/IndentStringBuilder.cs @@ -0,0 +1,51 @@ +using System.Text; + +namespace ConsoleAppFramework; + +internal class IndentStringBuilder(int level) +{ + StringBuilder builder = new StringBuilder(); + + public void Indent(int levelIncr = 1) + { + level += levelIncr; + } + + public void Unindent(int levelDecr = 1) + { + level -= levelDecr; + } + + public void AppendLine(string text) + { + if (level != 0) + { + builder.Append(' ', level * 4); // four spaces + } + builder.AppendLine(text); + } + + public void IndentAppendLine(string text) + { + Indent(); + AppendLine(text); + } + + public void UnindentAppendLine(string text) + { + Unindent(); + AppendLine(text); + } + + public void IndentAppendLineUnindent(string text) + { + Indent(); + AppendLine(text); + Unindent(); + } + + public override string ToString() + { + return builder.ToString(); + } +} \ No newline at end of file From 8c17aa64c69d6a0cf4a8cbf82422e386ed73d57b Mon Sep 17 00:00:00 2001 From: neuecc Date: Mon, 27 May 2024 13:41:35 +0900 Subject: [PATCH 25/54] wip --- sandbox/GeneratorSandbox/Program.cs | 11 +++-- src/ConsoleAppFramework5/Emitter.cs | 72 ++++++++++++++--------------- 2 files changed, 43 insertions(+), 40 deletions(-) diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index 2e214ff..fc968c8 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -26,14 +26,17 @@ // ConsoleApp.Run(args, Run2); void Run2(int x, int yzzzz) { }; +ConsoleApp.Run(args, ([Range(1, 10)] int x, int y) => +{ +}); -var builder = ConsoleApp.CreateBuilder(); +//evar builder = ConsoleApp.CreateBuilder(); -builder.Add(); -builder.Add("foo/tako", (int x, int y) => { return "foo"; }); +//builder.Add(); +//builder.Add("foo/tako", (int x, int y) => { return "foo"; }); -await builder.RunAsync(args); +//await builder.RunAsync(args); // var s = "foo"; // s.AsSpan().Split(',',). diff --git a/src/ConsoleAppFramework5/Emitter.cs b/src/ConsoleAppFramework5/Emitter.cs index 7c0b7bb..da86f7c 100644 --- a/src/ConsoleAppFramework5/Emitter.cs +++ b/src/ConsoleAppFramework5/Emitter.cs @@ -62,54 +62,54 @@ public string EmitRun(Command command, bool isRunAsync, string? methodName = nul } // parse argument(fast, switch directly) -> - var fastParseCase = new StringBuilder(); + var fastParseCase = new IndentStringBuilder(5); for (int i = 0; i < command.Parameters.Length; i++) { var parameter = command.Parameters[i]; if (!parameter.IsParsable) continue; if (parameter.IsArgument) continue; - fastParseCase.AppendLine($" case \"--{parameter.Name}\":"); + fastParseCase.AppendLine($"case \"--{parameter.Name}\":"); foreach (var alias in parameter.Aliases) { - fastParseCase.AppendLine($" case \"{alias}\":"); + fastParseCase.AppendLine($"case \"{alias}\":"); } - fastParseCase.AppendLine(" {"); - fastParseCase.AppendLine($" {parameter.BuildParseMethod(i, parameter.Name, wellKnownTypes, increment: true)}"); + fastParseCase.AppendLine("{"); + fastParseCase.IndentAppendLine($"{parameter.BuildParseMethod(i, parameter.Name, wellKnownTypes, increment: true)}"); if (!parameter.HasDefaultValue) { - fastParseCase.AppendLine($" arg{i}Parsed = true;"); + fastParseCase.AppendLine($"arg{i}Parsed = true;"); } - fastParseCase.AppendLine(" break;"); - fastParseCase.AppendLine(" }"); + fastParseCase.AppendLine("break;"); + fastParseCase.UnindentAppendLine("}"); } // parse argument(slow, if ignorecase) -> - var slowIgnoreCaseParse = new StringBuilder(); + var slowIgnoreCaseParse = new IndentStringBuilder(6); for (int i = 0; i < command.Parameters.Length; i++) { var parameter = command.Parameters[i]; if (!parameter.IsParsable) continue; if (parameter.IsArgument) continue; - slowIgnoreCaseParse.AppendLine($" if (string.Equals(name, \"--{parameter.Name}\", StringComparison.OrdinalIgnoreCase){(parameter.Aliases.Length == 0 ? ")" : "")}"); + slowIgnoreCaseParse.AppendLine($"if (string.Equals(name, \"--{parameter.Name}\", StringComparison.OrdinalIgnoreCase){(parameter.Aliases.Length == 0 ? ")" : "")}"); for (int j = 0; j < parameter.Aliases.Length; j++) { var alias = parameter.Aliases[j]; - slowIgnoreCaseParse.AppendLine($" || string.Equals(name, \"{alias}\", StringComparison.OrdinalIgnoreCase){(parameter.Aliases.Length == j + 1 ? ")" : "")}"); + slowIgnoreCaseParse.AppendLine($" || string.Equals(name, \"{alias}\", StringComparison.OrdinalIgnoreCase){(parameter.Aliases.Length == j + 1 ? ")" : "")}"); } - slowIgnoreCaseParse.AppendLine(" {"); - slowIgnoreCaseParse.AppendLine($" {parameter.BuildParseMethod(i, parameter.Name, wellKnownTypes, increment: true)}"); + slowIgnoreCaseParse.AppendLine("{"); + slowIgnoreCaseParse.IndentAppendLine($"{parameter.BuildParseMethod(i, parameter.Name, wellKnownTypes, increment: true)}"); if (!parameter.HasDefaultValue) { - slowIgnoreCaseParse.AppendLine($" arg{i}Parsed = true;"); + slowIgnoreCaseParse.AppendLine($"arg{i}Parsed = true;"); } - slowIgnoreCaseParse.AppendLine($" break;"); - slowIgnoreCaseParse.AppendLine(" }"); + slowIgnoreCaseParse.AppendLine($"break;"); + slowIgnoreCaseParse.UnindentAppendLine("}"); } // validate parsed -> - var validateParsed = new StringBuilder(); + var validateParsed = new IndentStringBuilder(3); for (int i = 0; i < command.Parameters.Length; i++) { var parameter = command.Parameters[i]; @@ -117,32 +117,32 @@ public string EmitRun(Command command, bool isRunAsync, string? methodName = nul if (!parameter.HasDefaultValue) { - validateParsed.AppendLine($" if (!arg{i}Parsed) ThrowRequiredArgumentNotParsed(\"{parameter.Name}\");"); + validateParsed.AppendLine($"if (!arg{i}Parsed) ThrowRequiredArgumentNotParsed(\"{parameter.Name}\");"); } } // hasValidation -> - var attributeValidation = new StringBuilder(); + var attributeValidation = new IndentStringBuilder(3); if (hasValidation) { - attributeValidation.AppendLine(" var validationContext = new System.ComponentModel.DataAnnotations.ValidationContext(\"\", null, null);"); - attributeValidation.AppendLine(" var parameters = command.Method.GetParameters();"); - attributeValidation.AppendLine(" System.Text.StringBuilder? errorMessages = null;"); + attributeValidation.AppendLine("var validationContext = new System.ComponentModel.DataAnnotations.ValidationContext(\"\", null, null);"); + attributeValidation.AppendLine("var parameters = command.Method.GetParameters();"); + attributeValidation.AppendLine("System.Text.StringBuilder? errorMessages = null;"); for (int i = 0; i < command.Parameters.Length; i++) { var parameter = command.Parameters[i]; if (!parameter.HasValidation) continue; - attributeValidation.AppendLine($" ValidateParameter(arg{i}, parameters[{i}], validationContext, ref errorMessages);"); + attributeValidation.AppendLine($"ValidateParameter(arg{i}, parameters[{i}], validationContext, ref errorMessages);"); } - attributeValidation.AppendLine(" if (errorMessages != null)"); - attributeValidation.AppendLine(" {"); - attributeValidation.AppendLine(" throw new System.ComponentModel.DataAnnotations.ValidationException(errorMessages.ToString());"); - attributeValidation.AppendLine(" }"); + attributeValidation.AppendLine("if (errorMessages != null)"); + attributeValidation.AppendLine("{"); + attributeValidation.IndentAppendLineUnindent("throw new System.ComponentModel.DataAnnotations.ValidationException(errorMessages.ToString());"); + attributeValidation.AppendLine("}"); } // invoke for sync/async, void/int - var invoke = new StringBuilder(); + var invoke = new IndentStringBuilder(3); var methodArguments = string.Join(", ", command.Parameters.Select((x, i) => $"arg{i}!")); string invokeCommand; if (command.CommandMethodInfo == null) @@ -165,7 +165,7 @@ public string EmitRun(Command command, bool isRunAsync, string? methodName = nul (false, false, false) => "" }; - invoke.AppendLine($" {usingInstance}var instance = {command.CommandMethodInfo.BuildNew()};"); + invoke.AppendLine($"{usingInstance}var instance = {command.CommandMethodInfo.BuildNew()};"); invokeCommand = $"instance.{command.CommandMethodInfo.MethodName}({methodArguments})"; } @@ -187,19 +187,19 @@ public string EmitRun(Command command, bool isRunAsync, string? methodName = nul if (command.IsVoid) { - invoke.AppendLine($" {invokeCommand};"); + invoke.AppendLine($"{invokeCommand};"); } else { - invoke.AppendLine($" Environment.ExitCode = {invokeCommand};"); + invoke.AppendLine($"Environment.ExitCode = {invokeCommand};"); } - invoke.AppendLine(" }"); // try close + invoke.UnindentAppendLine("}"); // try close if (hasCancellationToken) { - invoke.AppendLine(" catch (OperationCanceledException ex) when (ex.CancellationToken == posixSignalHandler.Token || ex.CancellationToken == posixSignalHandler.TimeoutToken)"); - invoke.AppendLine(" {"); - invoke.AppendLine(" Environment.ExitCode = 130;"); - invoke.AppendLine(" }"); + invoke.AppendLine("catch (OperationCanceledException ex) when (ex.CancellationToken == posixSignalHandler.Token || ex.CancellationToken == posixSignalHandler.TimeoutToken)"); + invoke.AppendLine("{"); + invoke.IndentAppendLineUnindent("Environment.ExitCode = 130;"); + invoke.AppendLine("}"); } var returnType = isRunAsync ? "async Task" : "void"; From 7c1091c21ef73184b9c9280516ae83dbd984952e Mon Sep 17 00:00:00 2001 From: neuecc Date: Mon, 27 May 2024 20:35:02 +0900 Subject: [PATCH 26/54] WIP SourceBuilder --- sandbox/GeneratorSandbox/Program.cs | 15 +- src/ConsoleAppFramework5/Command.cs | 4 +- src/ConsoleAppFramework5/Emitter.cs | 304 +++++++++--------- .../IndentStringBuilder.cs | 51 --- src/ConsoleAppFramework5/SourceBuilder.cs | 90 ++++++ 5 files changed, 249 insertions(+), 215 deletions(-) delete mode 100644 src/ConsoleAppFramework5/IndentStringBuilder.cs create mode 100644 src/ConsoleAppFramework5/SourceBuilder.cs diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index fc968c8..be33988 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -26,17 +26,18 @@ // ConsoleApp.Run(args, Run2); void Run2(int x, int yzzzz) { }; -ConsoleApp.Run(args, ([Range(1, 10)] int x, int y) => -{ -}); +//ConsoleApp.Run(args, ([Range(1, 10)] int x, int y) => +//{ +//}); -//evar builder = ConsoleApp.CreateBuilder(); +var builder = ConsoleApp.CreateBuilder(); -//builder.Add(); -//builder.Add("foo/tako", (int x, int y) => { return "foo"; }); +builder.Add(); +builder.Add("foo/tako", (int x, int y) => { return "foo"; }); +// builder.Add("foo/tako/ekkusu", (int x, int y, int z) => { return "foo"; }); -//await builder.RunAsync(args); +await builder.RunAsync(args); // var s = "foo"; // s.AsSpan().Split(',',). diff --git a/src/ConsoleAppFramework5/Command.cs b/src/ConsoleAppFramework5/Command.cs index 6a9d3d5..e6e641d 100644 --- a/src/ConsoleAppFramework5/Command.cs +++ b/src/ConsoleAppFramework5/Command.cs @@ -33,7 +33,7 @@ public record class Command public required DelegateBuildType DelegateBuildType { get; init; } public CommandMethodInfo? CommandMethodInfo { get; set; } // can set...! - public string BuildDelegateSignature(out string? delegateType) + public string? BuildDelegateSignature(out string? delegateType) { if (DelegateBuildType == DelegateBuildType.MakeDelegateWhenHasDefaultValue) { @@ -48,7 +48,7 @@ public string BuildDelegateSignature(out string? delegateType) if (DelegateBuildType == DelegateBuildType.None) { - return ""; + return null; } if (MethodKind == MethodKind.FunctionPointer) return BuildFunctionPointerDelegateSignature(); diff --git a/src/ConsoleAppFramework5/Emitter.cs b/src/ConsoleAppFramework5/Emitter.cs index da86f7c..1df01b8 100644 --- a/src/ConsoleAppFramework5/Emitter.cs +++ b/src/ConsoleAppFramework5/Emitter.cs @@ -15,7 +15,7 @@ public string EmitRun(Command command, bool isRunAsync, string? methodName = nul var parsableParameterCount = command.Parameters.Count(x => x.IsParsable); // prepare argument variables -> - var prepareArgument = new IndentStringBuilder(2); + var prepareArgument = new SourceBuilder(2); if (hasCancellationToken) { prepareArgument.AppendLine("using var posixSignalHandler = PosixSignalHandler.Register(Timeout);"); @@ -44,25 +44,26 @@ public string EmitRun(Command command, bool isRunAsync, string? methodName = nul } // parse indexed argument([Argument] parameter) - var indexedArgument = new IndentStringBuilder(4); + var indexedArgument = new SourceBuilder(4); for (int i = 0; i < command.Parameters.Length; i++) { var parameter = command.Parameters[i]; if (!parameter.IsArgument) continue; indexedArgument.AppendLine($"if (i == {parameter.ArgumentIndex})"); - indexedArgument.AppendLine("{"); - indexedArgument.IndentAppendLine($"{parameter.BuildParseMethod(i, parameter.Name, wellKnownTypes, increment: false)}"); - if (!parameter.HasDefaultValue) + using (indexedArgument.BeginBlock()) { - indexedArgument.AppendLine($"arg{i}Parsed = true;"); + indexedArgument.AppendLine($"{parameter.BuildParseMethod(i, parameter.Name, wellKnownTypes, increment: false)}"); + if (!parameter.HasDefaultValue) + { + indexedArgument.AppendLine($"arg{i}Parsed = true;"); + } + indexedArgument.AppendLine("continue;"); } - indexedArgument.AppendLine("continue;"); - indexedArgument.UnindentAppendLine("}"); } // parse argument(fast, switch directly) -> - var fastParseCase = new IndentStringBuilder(5); + var fastParseCase = new SourceBuilder(5); for (int i = 0; i < command.Parameters.Length; i++) { var parameter = command.Parameters[i]; @@ -74,18 +75,19 @@ public string EmitRun(Command command, bool isRunAsync, string? methodName = nul { fastParseCase.AppendLine($"case \"{alias}\":"); } - fastParseCase.AppendLine("{"); - fastParseCase.IndentAppendLine($"{parameter.BuildParseMethod(i, parameter.Name, wellKnownTypes, increment: true)}"); - if (!parameter.HasDefaultValue) + using (fastParseCase.BeginBlock()) { - fastParseCase.AppendLine($"arg{i}Parsed = true;"); + fastParseCase.AppendLine($"{parameter.BuildParseMethod(i, parameter.Name, wellKnownTypes, increment: true)}"); + if (!parameter.HasDefaultValue) + { + fastParseCase.AppendLine($"arg{i}Parsed = true;"); + } + fastParseCase.AppendLine("break;"); } - fastParseCase.AppendLine("break;"); - fastParseCase.UnindentAppendLine("}"); } // parse argument(slow, if ignorecase) -> - var slowIgnoreCaseParse = new IndentStringBuilder(6); + var slowIgnoreCaseParse = new SourceBuilder(6); for (int i = 0; i < command.Parameters.Length; i++) { var parameter = command.Parameters[i]; @@ -98,18 +100,19 @@ public string EmitRun(Command command, bool isRunAsync, string? methodName = nul var alias = parameter.Aliases[j]; slowIgnoreCaseParse.AppendLine($" || string.Equals(name, \"{alias}\", StringComparison.OrdinalIgnoreCase){(parameter.Aliases.Length == j + 1 ? ")" : "")}"); } - slowIgnoreCaseParse.AppendLine("{"); - slowIgnoreCaseParse.IndentAppendLine($"{parameter.BuildParseMethod(i, parameter.Name, wellKnownTypes, increment: true)}"); - if (!parameter.HasDefaultValue) + using (slowIgnoreCaseParse.BeginBlock()) { - slowIgnoreCaseParse.AppendLine($"arg{i}Parsed = true;"); + slowIgnoreCaseParse.AppendLine($"{parameter.BuildParseMethod(i, parameter.Name, wellKnownTypes, increment: true)}"); + if (!parameter.HasDefaultValue) + { + slowIgnoreCaseParse.AppendLine($"arg{i}Parsed = true;"); + } + slowIgnoreCaseParse.AppendLine($"break;"); } - slowIgnoreCaseParse.AppendLine($"break;"); - slowIgnoreCaseParse.UnindentAppendLine("}"); } // validate parsed -> - var validateParsed = new IndentStringBuilder(3); + var validateParsed = new SourceBuilder(3); for (int i = 0; i < command.Parameters.Length; i++) { var parameter = command.Parameters[i]; @@ -122,7 +125,7 @@ public string EmitRun(Command command, bool isRunAsync, string? methodName = nul } // hasValidation -> - var attributeValidation = new IndentStringBuilder(3); + var attributeValidation = new SourceBuilder(3); if (hasValidation) { attributeValidation.AppendLine("var validationContext = new System.ComponentModel.DataAnnotations.ValidationContext(\"\", null, null);"); @@ -136,13 +139,14 @@ public string EmitRun(Command command, bool isRunAsync, string? methodName = nul attributeValidation.AppendLine($"ValidateParameter(arg{i}, parameters[{i}], validationContext, ref errorMessages);"); } attributeValidation.AppendLine("if (errorMessages != null)"); - attributeValidation.AppendLine("{"); - attributeValidation.IndentAppendLineUnindent("throw new System.ComponentModel.DataAnnotations.ValidationException(errorMessages.ToString());"); - attributeValidation.AppendLine("}"); + using (attributeValidation.BeginBlock()) + { + attributeValidation.AppendLine("throw new System.ComponentModel.DataAnnotations.ValidationException(errorMessages.ToString());"); + } } // invoke for sync/async, void/int - var invoke = new IndentStringBuilder(3); + var invoke = new SourceBuilder(3); var methodArguments = string.Join(", ", command.Parameters.Select((x, i) => $"arg{i}!")); string invokeCommand; if (command.CommandMethodInfo == null) @@ -193,12 +197,16 @@ public string EmitRun(Command command, bool isRunAsync, string? methodName = nul { invoke.AppendLine($"Environment.ExitCode = {invokeCommand};"); } - invoke.UnindentAppendLine("}"); // try close + invoke.Unindent(); + invoke.AppendLine("}"); // try close if (hasCancellationToken) { invoke.AppendLine("catch (OperationCanceledException ex) when (ex.CancellationToken == posixSignalHandler.Token || ex.CancellationToken == posixSignalHandler.TimeoutToken)"); - invoke.AppendLine("{"); - invoke.IndentAppendLineUnindent("Environment.ExitCode = 130;"); + using (invoke.BeginBlock()) + { + invoke.AppendLine("Environment.ExitCode = 130;"); + } + invoke.Unindent(); invoke.AppendLine("}"); } @@ -209,7 +217,7 @@ public string EmitRun(Command command, bool isRunAsync, string? methodName = nul var unsafeCode = (command.MethodKind == MethodKind.FunctionPointer) ? "unsafe " : ""; var commandMethodType = command.BuildDelegateSignature(out var delegateType); - if (commandMethodType != "") + if (commandMethodType != null) { commandMethodType = $", {commandMethodType} command"; } @@ -268,159 +276,145 @@ public string EmitRun(Command command, bool isRunAsync, string? methodName = nul public string EmitBuilder(Command[] commands, bool emitSync, bool emitAsync) { - // TODO: make Add -> make Run -> make RunAsync - // TODO: invoke RootCommand - var fields = new StringBuilder(); - var addCase = new StringBuilder(); - var runCommands = new StringBuilder(); - var runAsyncCommands = new StringBuilder(); - var runCase = new StringBuilder(); - var runAsyncCase = new StringBuilder(); - var ids = commands.Select((x, i) => (x, i)).ToDictionary(x => x.x, x => x.i); - - void EmitBody(IEnumerable> groupedCommands, int depth) + // with id number + var commandIds = commands + .Select((x, i) => + { + return new CommandWithId( + FieldType: x.BuildDelegateSignature(out _), // for builder, always generate Action/Func so ok to ignore out var. + Command: x, + Id: i + ); + }) + .ToArray(); + + // grouped by path + var commandGroup = commandIds.ToLookup(x => x.Command.CommandPath.Length == 0 ? x.Command.CommandName : x.Command.CommandPath[0]); + + var sb = new SourceBuilder(1); + using (sb.BeginBlock("partial struct ConsoleAppBuilder")) { - // TODO: emitAsync - if (emitSync) + // fields: 'Action command0 = default!;' + foreach (var item in commandIds.Where(x => x.FieldType != null)) { - runCase.AppendLine($" switch (args[{depth}])"); - runCase.AppendLine($" {{"); + sb.AppendLine($"{item.FieldType} command{item.Id} = default!;"); + } - var implDefault = false; - foreach (var commands in groupedCommands) + // AddCore + sb.AppendLine(); + using (sb.BeginBlock("partial void AddCore(string commandName, Delegate command)")) + { + using (sb.BeginBlock("switch (commandName)")) { - if (commands.Key == null) implDefault = true; - - // runCase.AppendLine($" case:{commands.Key}"); - var key = commands.Key == null ? "default:" : $"case {commands.Key}:"; - runCase.AppendLine($" {key}"); - - if (implDefault) + foreach (var item in commandIds.Where(x => x.FieldType != null)) { - var command = commands.First(); // duplicate name is not allowed so always single command - var id = ids[command]; - string commandArgs = ""; - if (command.DelegateBuildType != DelegateBuildType.None) + using (sb.BeginIndent($"case \"{item.Command.CommandFullName}\":")) { - commandArgs = $", command{id}"; + sb.AppendLine($"this.command{item.Id} = Unsafe.As<{item.FieldType}>(command);"); + sb.AppendLine("break;"); } - runCase.AppendLine($" RunCommand{id}(args.AsSpan({depth + 1}){commandArgs});"); } - else + using (sb.BeginIndent("default:")) { - var nextDepth = depth + 1; - var nextGroup = commands.GroupBy(x => (x.CommandPath.Length < nextDepth) ? x.CommandName : x.CommandPath[nextDepth]); - EmitBody(nextGroup, nextDepth); // rec + sb.AppendLine("break;"); } - runCase.AppendLine($" break;"); - runCase.AppendLine($" }}"); - } - - if (!implDefault) - { - runCase.AppendLine($" default:"); - runCase.AppendLine($" break;"); } - runCase.AppendLine($" }}"); - } - } - - // TODO:WIP - // EmitBody(commands.GroupBy(x => x.CommandPath.Length == 0 ? x.CommandName : x.CommandPath[0]), 0); - - - - for (int i = 0; i < commands.Length; i++) - { - var command = commands[i]; - - string commandArgs = ""; - if (command.DelegateBuildType != DelegateBuildType.None) - { - var fieldType = command.BuildDelegateSignature(out _); // for builder, always generate Action/Func. - fields.AppendLine($" {fieldType} command{i} = default!;"); - - addCase.AppendLine($" case \"{command.CommandFullName}\":"); - addCase.AppendLine($" this.command{i} = Unsafe.As<{fieldType}>(command);"); - addCase.AppendLine($" break;"); - - commandArgs = $", command{i}"; } + // RunCore if (emitSync) { - runCommands.AppendLine(EmitRun(command, false, $"RunCommand{i}")); - - runCase.AppendLine($" case \"{command.CommandFullName}\":"); - runCase.AppendLine($" RunCommand{i}(args.AsSpan(1){commandArgs});"); - runCase.AppendLine($" break;"); + sb.AppendLine(); + using (sb.BeginBlock("partial void RunCore(string[] args)")) + { + EmitRunBody(commandGroup, 0, false); + } } + // RunAsyncCore if (emitAsync) { - runAsyncCommands.AppendLine(EmitRun(command, true, $"RunAsyncCommand{i}")); - - runAsyncCase.AppendLine($" case \"{command.CommandFullName}\":"); - runAsyncCase.AppendLine($" result = RunAsyncCommand{i}(args[1..]{commandArgs});"); - runAsyncCase.AppendLine($" break;"); + sb.AppendLine(); + using (sb.BeginBlock("partial void RunAsyncCore(string[] args, ref Task result)")) + { + EmitRunBody(commandGroup, 0, true); + } } } - var addCore = $$""" - partial void AddCore(string commandName, Delegate command) - { - switch (commandName) - { -{{addCase}} - default: - break; - } - } -"""; + // emit outside of ConsoleAppBuilder - var runCore = $$""" - partial void RunCore(string[] args) - { - switch (args[0]) + // static sync command function + if (emitSync) { -{{runCase}} - default: - break; + sb.AppendLine(); + foreach (var item in commandIds) + { + sb.AppendLine(EmitRun(item.Command, false, $"RunCommand{item.Id}").TrimStart()); + } } - } -"""; - var runAsyncCore = $$""" - partial void RunAsyncCore(string[] args, ref Task result) - { - switch (args[0]) + // static async command function + if (emitAsync) { -{{runAsyncCase}} - default: - break; + sb.AppendLine(); + foreach (var item in commandIds) + { + sb.AppendLine(EmitRun(item.Command, true, $"RunAsyncCommand{item.Id}").TrimStart()); + } } - } -"""; - - if (!emitSync) runCore = ""; - if (!emitAsync) runAsyncCore = ""; - - // TODO: Emit help and version - var code = $$""" -partial struct ConsoleAppBuilder -{ -{{fields}} -{{addCore}} + return sb.ToString(); -{{runCommands}} -{{runAsyncCommands}} - -{{runCore}} -{{runAsyncCore}} -} -"""; + void EmitRunBody(IEnumerable> groupedCommands, int depth, bool isRunAsync) + { + using (sb.BeginBlock($"switch (args[{depth}])")) + { + // case:... + foreach (var commands in groupedCommands) + { + using (sb.BeginIndent($"case \"{commands.Key}\":")) + { + // TODO: check leaf command + if (commands.Count() != 1) + { + // recursive: next depth + var nextDepth = depth + 1; + var nextGroup = commands.GroupBy(x => x.Command.CommandPath.Length < nextDepth ? x.Command.CommandName : x.Command.CommandPath[nextDepth]); + EmitRunBody(nextGroup, nextDepth, isRunAsync); + sb.AppendLine("break;"); + } + else + { + var cmd = commands.First(); + + string commandArgs = ""; + if (cmd.Command.DelegateBuildType != DelegateBuildType.None) + { + commandArgs = $", command{cmd.Id}"; + } + + if (!isRunAsync) + { + sb.AppendLine($"RunCommand{cmd.Id}(args.AsSpan({depth + 1}){commandArgs});"); + } + else + { + sb.AppendLine($"result = RunAsyncCommand{cmd.Id}(args[1..]{commandArgs});"); + } + sb.AppendLine("break;"); + } + } + } - return code; + // TODO: invoke root command + using (sb.BeginIndent("default:")) + { + sb.AppendLine("break;"); + } + } + } } } + +internal record CommandWithId(string? FieldType, Command Command, int Id); diff --git a/src/ConsoleAppFramework5/IndentStringBuilder.cs b/src/ConsoleAppFramework5/IndentStringBuilder.cs deleted file mode 100644 index 5bbbd46..0000000 --- a/src/ConsoleAppFramework5/IndentStringBuilder.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Text; - -namespace ConsoleAppFramework; - -internal class IndentStringBuilder(int level) -{ - StringBuilder builder = new StringBuilder(); - - public void Indent(int levelIncr = 1) - { - level += levelIncr; - } - - public void Unindent(int levelDecr = 1) - { - level -= levelDecr; - } - - public void AppendLine(string text) - { - if (level != 0) - { - builder.Append(' ', level * 4); // four spaces - } - builder.AppendLine(text); - } - - public void IndentAppendLine(string text) - { - Indent(); - AppendLine(text); - } - - public void UnindentAppendLine(string text) - { - Unindent(); - AppendLine(text); - } - - public void IndentAppendLineUnindent(string text) - { - Indent(); - AppendLine(text); - Unindent(); - } - - public override string ToString() - { - return builder.ToString(); - } -} \ No newline at end of file diff --git a/src/ConsoleAppFramework5/SourceBuilder.cs b/src/ConsoleAppFramework5/SourceBuilder.cs new file mode 100644 index 0000000..b1a8d90 --- /dev/null +++ b/src/ConsoleAppFramework5/SourceBuilder.cs @@ -0,0 +1,90 @@ +using System.Text; + +namespace ConsoleAppFramework; + +// indent-level: 0. using namespace, class, struct declaration +// indent-level: 1. field, property, method declaration +// indent-level: 2. method body +internal class SourceBuilder(int level) +{ + StringBuilder builder = new StringBuilder(); + + public void Indent(int levelIncr = 1) + { + level += levelIncr; + } + + public void Unindent(int levelDecr = 1) + { + level -= levelDecr; + } + + public Scope BeginIndent() + { + Indent(); + return new Scope(this); + } + + public Scope BeginIndent(string code) + { + AppendLine(code); + Indent(); + return new Scope(this); + } + + public Block BeginBlock() + { + AppendLine("{"); + Indent(); + return new Block(this); + } + + public Block BeginBlock(string code) + { + AppendLine(code); + AppendLine("{"); + Indent(); + return new Block(this); + } + + public void AppendLine() + { + builder.AppendLine(); + } + + public void AppendLineIfExists(T[] values) + { + if (values.Length != 0) + { + builder.AppendLine(); + } + } + + public void AppendLine(string text) + { + if (level != 0) + { + builder.Append(' ', level * 4); // four spaces + } + builder.AppendLine(text); + } + + public override string ToString() => builder.ToString(); + + public struct Scope(SourceBuilder parent) : IDisposable + { + public void Dispose() + { + parent.Unindent(); + } + } + + public struct Block(SourceBuilder parent) : IDisposable + { + public void Dispose() + { + parent.Unindent(); + parent.AppendLine("}"); + } + } +} \ No newline at end of file From 0d3db298727015418a376f853f7c099dbfda9572 Mon Sep 17 00:00:00 2001 From: neuecc Date: Mon, 27 May 2024 21:18:44 +0900 Subject: [PATCH 27/54] refactoring emitter --- sandbox/GeneratorSandbox/Program.cs | 45 ++ .../ConsoleAppGenerator.cs | 113 ++--- src/ConsoleAppFramework5/Emitter.cs | 479 +++++++++--------- 3 files changed, 324 insertions(+), 313 deletions(-) diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index be33988..be5c2ec 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -378,6 +378,51 @@ namespace ConsoleAppFramework partial class ConsoleApp { + private static async Task RunAsyncCommand1(string[] args) + { + if (TryShowHelpOrVersion(args, 0)) return; + + using var posixSignalHandler = PosixSignalHandler.Register(Timeout); + var arg0 = posixSignalHandler.Token; + + try + { + for (int i = 0; i < args.Length; i++) + { + var name = args[i]; + + switch (name) + { + default: + ThrowArgumentNameNotFound(name); + break; + } + } + var instance = new global::MyClass(); + await Task.Run(() => instance.Do(arg0!)).WaitAsync(posixSignalHandler.TimeoutToken); + } + catch (Exception ex) + { + if ((ex is OperationCanceledException oce) && (oce.CancellationToken == posixSignalHandler.Token || oce.CancellationToken == posixSignalHandler.TimeoutToken)) + { + Environment.ExitCode = 130; + return; + } + + Environment.ExitCode = 1; + if (ex is System.ComponentModel.DataAnnotations.ValidationException) + { + LogError(ex.Message); + } + else + { + LogError(ex.ToString()); + } + } + } + + + public struct Builder() { private static void RunCommand0(ReadOnlySpan args) diff --git a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs index 8a520b0..52b6446 100644 --- a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs @@ -382,26 +382,7 @@ static void EmitConsoleAppTemplateSource(IncrementalGeneratorPostInitializationC context.AddSource("ConsoleApp.cs", ConsoleAppBaseCode); } - static void EmitConsoleAppRun(SourceProductionContext sourceProductionContext, (InvocationExpressionSyntax, SemanticModel) generatorSyntaxContext) - { - var node = generatorSyntaxContext.Item1; - var model = generatorSyntaxContext.Item2; - - var wellKnownTypes = new WellKnownTypes(model.Compilation); - - var parser = new Parser(sourceProductionContext, node, model, wellKnownTypes, DelegateBuildType.MakeDelegateWhenHasDefaultValue); - var command = parser.ParseAndValidate(); - if (command == null) - { - return; - } - - var emitter = new Emitter(wellKnownTypes); - - var isRunAsync = ((node.Expression as MemberAccessExpressionSyntax)?.Name.Identifier.Text == "RunAsync"); - var code = emitter.EmitRun(command, isRunAsync); - - sourceProductionContext.AddSource("ConsoleApp.Run.g.cs", $$""" + const string GeneratedCodeHeader = """ // #nullable enable #pragma warning disable CS0108 // hides inherited member @@ -419,17 +400,47 @@ static void EmitConsoleAppRun(SourceProductionContext sourceProductionContext, ( #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 - + 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; -internal static partial class ConsoleApp -{ -{{code}} -} -"""); +"""; + + static void EmitConsoleAppRun(SourceProductionContext sourceProductionContext, (InvocationExpressionSyntax, SemanticModel) generatorSyntaxContext) + { + var node = generatorSyntaxContext.Item1; + var model = generatorSyntaxContext.Item2; + + var wellKnownTypes = new WellKnownTypes(model.Compilation); + + var parser = new Parser(sourceProductionContext, node, model, wellKnownTypes, DelegateBuildType.MakeDelegateWhenHasDefaultValue); + var command = parser.ParseAndValidate(); + if (command == null) + { + return; + } + + var isRunAsync = ((node.Expression as MemberAccessExpressionSyntax)?.Name.Identifier.Text == "RunAsync"); + + + var sb = new SourceBuilder(0); + sb.AppendLine(GeneratedCodeHeader); + using (sb.BeginBlock("internal static partial class ConsoleApp")) + { + var emitter = new Emitter(wellKnownTypes); + emitter.EmitRun(sb, command, isRunAsync); + } + + sourceProductionContext.AddSource("ConsoleApp.Builder.g.cs", sb.ToString()); } static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContext, ImmutableArray<(InvocationExpressionSyntax Node, string Name, SemanticModel Model)> generatorSyntaxContexts) @@ -516,47 +527,15 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex if (!hasRun && !hasRunAsync) return; - var emitter = new Emitter(wellKnownTypes); - - var code = emitter.EmitBuilder(commands!, hasRun, hasRunAsync); + var sb = new SourceBuilder(0); + sb.AppendLine(GeneratedCodeHeader); - sourceProductionContext.AddSource("ConsoleApp.Builder.g.cs", $$""" -// -#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 - -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; - -internal static partial class ConsoleApp -{ - -{{code}} + using (sb.BeginBlock("internal static partial class ConsoleApp")) + { + var emitter = new Emitter(wellKnownTypes); + emitter.EmitBuilder(sb, commands!, hasRun, hasRunAsync); + } -} -"""); + sourceProductionContext.AddSource("ConsoleApp.Builder.g.cs", sb.ToString()); } } \ No newline at end of file diff --git a/src/ConsoleAppFramework5/Emitter.cs b/src/ConsoleAppFramework5/Emitter.cs index 1df01b8..a89dcde 100644 --- a/src/ConsoleAppFramework5/Emitter.cs +++ b/src/ConsoleAppFramework5/Emitter.cs @@ -1,12 +1,10 @@ using Microsoft.CodeAnalysis; -using System.Reflection.Metadata; -using System.Text; namespace ConsoleAppFramework; internal class Emitter(WellKnownTypes wellKnownTypes) { - public string EmitRun(Command command, bool isRunAsync, string? methodName = null) + public void EmitRun(SourceBuilder sb, Command command, bool isRunAsync, string? methodName = null) { var emitForBuilder = methodName != null; var hasCancellationToken = command.Parameters.Any(x => x.IsCancellationToken); @@ -14,267 +12,260 @@ public string EmitRun(Command command, bool isRunAsync, string? methodName = nul var hasValidation = command.Parameters.Any(x => x.HasValidation); var parsableParameterCount = command.Parameters.Count(x => x.IsParsable); - // prepare argument variables -> - var prepareArgument = new SourceBuilder(2); - if (hasCancellationToken) - { - prepareArgument.AppendLine("using var posixSignalHandler = PosixSignalHandler.Register(Timeout);"); - } - for (var i = 0; i < command.Parameters.Length; i++) + var returnType = isRunAsync ? "async Task" : "void"; + var accessibility = !emitForBuilder ? "public" : "private"; + var argsType = !emitForBuilder ? "string[]" : (isRunAsync ? "string[]" : "ReadOnlySpan"); // NOTE: C# 13 will allow Span in async methods so can change to ReadOnlyMemory(and store .Span in local var) + methodName = methodName ?? (isRunAsync ? "RunAsync" : "Run"); + var unsafeCode = (command.MethodKind == MethodKind.FunctionPointer) ? "unsafe " : ""; + + var commandMethodType = command.BuildDelegateSignature(out var delegateType); + if (commandMethodType != null) { - var parameter = command.Parameters[i]; - if (parameter.IsParsable) - { - var defaultValue = parameter.HasDefaultValue ? parameter.DefaultValueToString() : $"default({parameter.Type.ToFullyQualifiedFormatDisplayString()})"; - prepareArgument.AppendLine($"var arg{i} = {defaultValue};"); - if (!parameter.HasDefaultValue) - { - prepareArgument.AppendLine($"var arg{i}Parsed = false;"); - } - } - else if (parameter.IsCancellationToken) - { - prepareArgument.AppendLine($"var arg{i} = posixSignalHandler.Token;"); - } - else if (parameter.IsFromServices) - { - var type = parameter.Type.ToFullyQualifiedFormatDisplayString(); - prepareArgument.AppendLine($"var arg{i} = ({type})ServiceProvider!.GetService(typeof({type}))!;"); - } + commandMethodType = $", {commandMethodType} command"; } - // parse indexed argument([Argument] parameter) - var indexedArgument = new SourceBuilder(4); - for (int i = 0; i < command.Parameters.Length; i++) + // emit custom delegate type + if (delegateType != null && !emitForBuilder) { - var parameter = command.Parameters[i]; - if (!parameter.IsArgument) continue; - - indexedArgument.AppendLine($"if (i == {parameter.ArgumentIndex})"); - using (indexedArgument.BeginBlock()) - { - indexedArgument.AppendLine($"{parameter.BuildParseMethod(i, parameter.Name, wellKnownTypes, increment: false)}"); - if (!parameter.HasDefaultValue) - { - indexedArgument.AppendLine($"arg{i}Parsed = true;"); - } - indexedArgument.AppendLine("continue;"); - } + sb.AppendLine($"internal {{delgateType}}"); + sb.AppendLine(); } - // parse argument(fast, switch directly) -> - var fastParseCase = new SourceBuilder(5); - for (int i = 0; i < command.Parameters.Length; i++) + // method signature + using (sb.BeginBlock($"{accessibility} static {unsafeCode}{returnType} {methodName}({argsType} args{commandMethodType})")) { - var parameter = command.Parameters[i]; - if (!parameter.IsParsable) continue; - if (parameter.IsArgument) continue; + sb.AppendLine($"if (TryShowHelpOrVersion(args, {parsableParameterCount})) return;"); + sb.AppendLine(); - fastParseCase.AppendLine($"case \"--{parameter.Name}\":"); - foreach (var alias in parameter.Aliases) + // prepare argument variables + if (hasCancellationToken) { - fastParseCase.AppendLine($"case \"{alias}\":"); + sb.AppendLine("using var posixSignalHandler = PosixSignalHandler.Register(Timeout);"); } - using (fastParseCase.BeginBlock()) + for (var i = 0; i < command.Parameters.Length; i++) { - fastParseCase.AppendLine($"{parameter.BuildParseMethod(i, parameter.Name, wellKnownTypes, increment: true)}"); - if (!parameter.HasDefaultValue) + var parameter = command.Parameters[i]; + if (parameter.IsParsable) { - fastParseCase.AppendLine($"arg{i}Parsed = true;"); + var defaultValue = parameter.HasDefaultValue ? parameter.DefaultValueToString() : $"default({parameter.Type.ToFullyQualifiedFormatDisplayString()})"; + sb.AppendLine($"var arg{i} = {defaultValue};"); + if (!parameter.HasDefaultValue) + { + sb.AppendLine($"var arg{i}Parsed = false;"); + } + } + else if (parameter.IsCancellationToken) + { + sb.AppendLine($"var arg{i} = posixSignalHandler.Token;"); + } + else if (parameter.IsFromServices) + { + var type = parameter.Type.ToFullyQualifiedFormatDisplayString(); + sb.AppendLine($"var arg{i} = ({type})ServiceProvider!.GetService(typeof({type}))!;"); } - fastParseCase.AppendLine("break;"); } - } + sb.AppendLineIfExists(command.Parameters); - // parse argument(slow, if ignorecase) -> - var slowIgnoreCaseParse = new SourceBuilder(6); - for (int i = 0; i < command.Parameters.Length; i++) - { - var parameter = command.Parameters[i]; - if (!parameter.IsParsable) continue; - if (parameter.IsArgument) continue; - - slowIgnoreCaseParse.AppendLine($"if (string.Equals(name, \"--{parameter.Name}\", StringComparison.OrdinalIgnoreCase){(parameter.Aliases.Length == 0 ? ")" : "")}"); - for (int j = 0; j < parameter.Aliases.Length; j++) - { - var alias = parameter.Aliases[j]; - slowIgnoreCaseParse.AppendLine($" || string.Equals(name, \"{alias}\", StringComparison.OrdinalIgnoreCase){(parameter.Aliases.Length == j + 1 ? ")" : "")}"); - } - using (slowIgnoreCaseParse.BeginBlock()) + // try TODO: if using filter, does not emit try + using (sb.BeginBlock("try")) { - slowIgnoreCaseParse.AppendLine($"{parameter.BuildParseMethod(i, parameter.Name, wellKnownTypes, increment: true)}"); - if (!parameter.HasDefaultValue) + using (sb.BeginBlock("for (int i = 0; i < args.Length; i++)")) { - slowIgnoreCaseParse.AppendLine($"arg{i}Parsed = true;"); - } - slowIgnoreCaseParse.AppendLine($"break;"); - } - } + // parse indexed argument([Argument] parameter) + if (hasArgument) + { + for (int i = 0; i < command.Parameters.Length; i++) + { + var parameter = command.Parameters[i]; + if (!parameter.IsArgument) continue; - // validate parsed -> - var validateParsed = new SourceBuilder(3); - for (int i = 0; i < command.Parameters.Length; i++) - { - var parameter = command.Parameters[i]; - if (!parameter.IsParsable) continue; + sb.AppendLine($"if (i == {parameter.ArgumentIndex})"); + using (sb.BeginBlock()) + { + sb.AppendLine($"{parameter.BuildParseMethod(i, parameter.Name, wellKnownTypes, increment: false)}"); + if (!parameter.HasDefaultValue) + { + sb.AppendLine($"arg{i}Parsed = true;"); + } + sb.AppendLine("continue;"); + } + } + sb.AppendLine(); + } - if (!parameter.HasDefaultValue) - { - validateParsed.AppendLine($"if (!arg{i}Parsed) ThrowRequiredArgumentNotParsed(\"{parameter.Name}\");"); - } - } + sb.AppendLine("var name = args[i];"); + sb.AppendLine(); - // hasValidation -> - var attributeValidation = new SourceBuilder(3); - if (hasValidation) - { - attributeValidation.AppendLine("var validationContext = new System.ComponentModel.DataAnnotations.ValidationContext(\"\", null, null);"); - attributeValidation.AppendLine("var parameters = command.Method.GetParameters();"); - attributeValidation.AppendLine("System.Text.StringBuilder? errorMessages = null;"); - for (int i = 0; i < command.Parameters.Length; i++) - { - var parameter = command.Parameters[i]; - if (!parameter.HasValidation) continue; + using (sb.BeginBlock("switch (name)")) + { + // parse argument(fast, switch directly) + for (int i = 0; i < command.Parameters.Length; i++) + { + var parameter = command.Parameters[i]; + if (!parameter.IsParsable) continue; + if (parameter.IsArgument) continue; - attributeValidation.AppendLine($"ValidateParameter(arg{i}, parameters[{i}], validationContext, ref errorMessages);"); - } - attributeValidation.AppendLine("if (errorMessages != null)"); - using (attributeValidation.BeginBlock()) - { - attributeValidation.AppendLine("throw new System.ComponentModel.DataAnnotations.ValidationException(errorMessages.ToString());"); - } - } + sb.AppendLine($"case \"--{parameter.Name}\":"); + foreach (var alias in parameter.Aliases) + { + sb.AppendLine($"case \"{alias}\":"); + } + using (sb.BeginBlock()) + { + sb.AppendLine($"{parameter.BuildParseMethod(i, parameter.Name, wellKnownTypes, increment: true)}"); + if (!parameter.HasDefaultValue) + { + sb.AppendLine($"arg{i}Parsed = true;"); + } + sb.AppendLine("break;"); + } + } - // invoke for sync/async, void/int - var invoke = new SourceBuilder(3); - var methodArguments = string.Join(", ", command.Parameters.Select((x, i) => $"arg{i}!")); - string invokeCommand; - if (command.CommandMethodInfo == null) - { - invokeCommand = $"command({methodArguments})"; - } - else - { - var usingInstance = (isRunAsync, command.CommandMethodInfo.IsIDisposable, command.CommandMethodInfo.IsIAsyncDisposable) switch - { - // awaitable - (true, true, true) => "await using ", - (true, true, false) => "using ", - (true, false, true) => "await using ", - (true, false, false) => "", - // sync - (false, true, true) => "using ", - (false, true, false) => "using ", - (false, false, true) => "", // IAsyncDisposable but sync, can't call disposeasync...... - (false, false, false) => "" - }; - - invoke.AppendLine($"{usingInstance}var instance = {command.CommandMethodInfo.BuildNew()};"); - invokeCommand = $"instance.{command.CommandMethodInfo.MethodName}({methodArguments})"; - } + using (sb.BeginIndent("default:")) + { + // parse argument(slow, ignorecase) + for (int i = 0; i < command.Parameters.Length; i++) + { + var parameter = command.Parameters[i]; + if (!parameter.IsParsable) continue; + if (parameter.IsArgument) continue; + + sb.AppendLine($"if (string.Equals(name, \"--{parameter.Name}\", StringComparison.OrdinalIgnoreCase){(parameter.Aliases.Length == 0 ? ")" : "")}"); + for (int j = 0; j < parameter.Aliases.Length; j++) + { + var alias = parameter.Aliases[j]; + sb.AppendLine($" || string.Equals(name, \"{alias}\", StringComparison.OrdinalIgnoreCase){(parameter.Aliases.Length == j + 1 ? ")" : "")}"); + } + using (sb.BeginBlock()) + { + sb.AppendLine($"{parameter.BuildParseMethod(i, parameter.Name, wellKnownTypes, increment: true)}"); + if (!parameter.HasDefaultValue) + { + sb.AppendLine($"arg{i}Parsed = true;"); + } + sb.AppendLine($"break;"); + } + } - if (hasCancellationToken) - { - invokeCommand = $"Task.Run(() => {invokeCommand}).WaitAsync(posixSignalHandler.TimeoutToken)"; - } - if (command.IsAsync || hasCancellationToken) - { - if (isRunAsync) - { - invokeCommand = $"await {invokeCommand}"; - } - else - { - invokeCommand = $"{invokeCommand}.GetAwaiter().GetResult()"; - } - } + sb.AppendLine("ThrowArgumentNameNotFound(name);"); + sb.AppendLine("break;"); + } + } + } - if (command.IsVoid) - { - invoke.AppendLine($"{invokeCommand};"); - } - else - { - invoke.AppendLine($"Environment.ExitCode = {invokeCommand};"); - } - invoke.Unindent(); - invoke.AppendLine("}"); // try close - if (hasCancellationToken) - { - invoke.AppendLine("catch (OperationCanceledException ex) when (ex.CancellationToken == posixSignalHandler.Token || ex.CancellationToken == posixSignalHandler.TimeoutToken)"); - using (invoke.BeginBlock()) - { - invoke.AppendLine("Environment.ExitCode = 130;"); - } - invoke.Unindent(); - invoke.AppendLine("}"); - } + // validate parsed + for (int i = 0; i < command.Parameters.Length; i++) + { + var parameter = command.Parameters[i]; + if (!parameter.IsParsable) continue; - var returnType = isRunAsync ? "async Task" : "void"; - var accessibility = !emitForBuilder ? "public" : "private"; - var argsType = !emitForBuilder ? "string[]" : (isRunAsync ? "string[]" : "ReadOnlySpan"); // NOTE: C# 13 will allow Span in async methods so can change to ReadOnlyMemory(and store .Span in local var) - methodName = methodName ?? (isRunAsync ? "RunAsync" : "Run"); - var unsafeCode = (command.MethodKind == MethodKind.FunctionPointer) ? "unsafe " : ""; + if (!parameter.HasDefaultValue) + { + sb.AppendLine($"if (!arg{i}Parsed) ThrowRequiredArgumentNotParsed(\"{parameter.Name}\");"); + } + } - var commandMethodType = command.BuildDelegateSignature(out var delegateType); - if (commandMethodType != null) - { - commandMethodType = $", {commandMethodType} command"; - } + // attribute validation + if (hasValidation) + { + sb.AppendLine(); + sb.AppendLine("var validationContext = new System.ComponentModel.DataAnnotations.ValidationContext(\"\", null, null);"); + sb.AppendLine("var parameters = command.Method.GetParameters();"); + sb.AppendLine("System.Text.StringBuilder? errorMessages = null;"); + for (int i = 0; i < command.Parameters.Length; i++) + { + var parameter = command.Parameters[i]; + if (!parameter.HasValidation) continue; - var code = $$""" - {{accessibility}} static {{unsafeCode}}{{returnType}} {{methodName}}({{argsType}} args{{commandMethodType}}) - { - if (TryShowHelpOrVersion(args, {{parsableParameterCount}})) return; + sb.AppendLine($"ValidateParameter(arg{i}, parameters[{i}], validationContext, ref errorMessages);"); + } + sb.AppendLine("if (errorMessages != null)"); + using (sb.BeginBlock()) + { + sb.AppendLine("throw new System.ComponentModel.DataAnnotations.ValidationException(errorMessages.ToString());"); + } + } -{{prepareArgument}} - try - { - for (int i = 0; i < args.Length; i++) - { -{{indexedArgument}} - var name = args[i]; + // invoke for sync/async, void/int + sb.AppendLine(); + var methodArguments = string.Join(", ", command.Parameters.Select((x, i) => $"arg{i}!")); + string invokeCommand; + if (command.CommandMethodInfo == null) + { + invokeCommand = $"command({methodArguments})"; + } + else + { + var usingInstance = (isRunAsync, command.CommandMethodInfo.IsIDisposable, command.CommandMethodInfo.IsIAsyncDisposable) switch + { + // awaitable + (true, true, true) => "await using ", + (true, true, false) => "using ", + (true, false, true) => "await using ", + (true, false, false) => "", + // sync + (false, true, true) => "using ", + (false, true, false) => "using ", + (false, false, true) => "", // IAsyncDisposable but sync, can't call disposeasync...... + (false, false, false) => "" + }; + + sb.AppendLine($"{usingInstance}var instance = {command.CommandMethodInfo.BuildNew()};"); + invokeCommand = $"instance.{command.CommandMethodInfo.MethodName}({methodArguments})"; + } - switch (name) + if (hasCancellationToken) { -{{fastParseCase}} - default: -{{slowIgnoreCaseParse}} - ThrowArgumentNameNotFound(name); - break; + invokeCommand = $"Task.Run(() => {invokeCommand}).WaitAsync(posixSignalHandler.TimeoutToken)"; + } + if (command.IsAsync || hasCancellationToken) + { + if (isRunAsync) + { + invokeCommand = $"await {invokeCommand}"; + } + else + { + invokeCommand = $"{invokeCommand}.GetAwaiter().GetResult()"; + } + } + + if (command.IsVoid) + { + sb.AppendLine($"{invokeCommand};"); + } + else + { + sb.AppendLine($"Environment.ExitCode = {invokeCommand};"); } } -{{validateParsed}} -{{attributeValidation}} -{{invoke}} - catch (Exception ex) - { - Environment.ExitCode = 1; - if (ex is System.ComponentModel.DataAnnotations.ValidationException) - { - LogError(ex.Message); - } - else + using (sb.BeginBlock("catch (Exception ex)")) { - LogError(ex.ToString()); - } - } - } -"""; - - if (delegateType != null && !emitForBuilder) - { - code += $$""" + if (hasCancellationToken) + { + using (sb.BeginBlock("if ((ex is OperationCanceledException oce) && (oce.CancellationToken == posixSignalHandler.Token || oce.CancellationToken == posixSignalHandler.TimeoutToken))")) + { + sb.AppendLine("Environment.ExitCode = 130;"); + sb.AppendLine("return;"); + } + sb.AppendLine(); + } + sb.AppendLine("Environment.ExitCode = 1;"); - internal {{delegateType}} -"""; + using (sb.BeginBlock("if (ex is ValidationException)")) + { + sb.AppendLine("LogError(ex.Message);"); + } + using (sb.BeginBlock("else")) + { + sb.AppendLine("LogError(ex.ToString());"); + } + } } - - return code; } - public string EmitBuilder(Command[] commands, bool emitSync, bool emitAsync) + public void EmitBuilder(SourceBuilder sb, Command[] commands, bool emitSync, bool emitAsync) { // with id number var commandIds = commands @@ -291,7 +282,6 @@ public string EmitBuilder(Command[] commands, bool emitSync, bool emitAsync) // grouped by path var commandGroup = commandIds.ToLookup(x => x.Command.CommandPath.Length == 0 ? x.Command.CommandName : x.Command.CommandPath[0]); - var sb = new SourceBuilder(1); using (sb.BeginBlock("partial struct ConsoleAppBuilder")) { // fields: 'Action command0 = default!;' @@ -340,32 +330,28 @@ public string EmitBuilder(Command[] commands, bool emitSync, bool emitAsync) EmitRunBody(commandGroup, 0, true); } } - } - - // emit outside of ConsoleAppBuilder - // static sync command function - if (emitSync) - { - sb.AppendLine(); - foreach (var item in commandIds) + // static sync command function + if (emitSync) { - sb.AppendLine(EmitRun(item.Command, false, $"RunCommand{item.Id}").TrimStart()); + sb.AppendLine(); + foreach (var item in commandIds) + { + EmitRun(sb, item.Command, false, $"RunCommand{item.Id}"); + } } - } - // static async command function - if (emitAsync) - { - sb.AppendLine(); - foreach (var item in commandIds) + // static async command function + if (emitAsync) { - sb.AppendLine(EmitRun(item.Command, true, $"RunAsyncCommand{item.Id}").TrimStart()); + sb.AppendLine(); + foreach (var item in commandIds) + { + EmitRun(sb, item.Command, true, $"RunAsyncCommand{item.Id}"); + } } } - return sb.ToString(); - void EmitRunBody(IEnumerable> groupedCommands, int depth, bool isRunAsync) { using (sb.BeginBlock($"switch (args[{depth}])")) @@ -415,6 +401,7 @@ void EmitRunBody(IEnumerable> groupedCommands, } } } + + internal record CommandWithId(string? FieldType, Command Command, int Id); } -internal record CommandWithId(string? FieldType, Command Command, int Id); From 3dffd29915a52a319ea8d5e80cd5ea0f5c98d79d Mon Sep 17 00:00:00 2001 From: neuecc Date: Mon, 27 May 2024 21:35:57 +0900 Subject: [PATCH 28/54] nest command phase 1 --- src/ConsoleAppFramework5/Emitter.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/ConsoleAppFramework5/Emitter.cs b/src/ConsoleAppFramework5/Emitter.cs index a89dcde..fe5c151 100644 --- a/src/ConsoleAppFramework5/Emitter.cs +++ b/src/ConsoleAppFramework5/Emitter.cs @@ -361,18 +361,18 @@ void EmitRunBody(IEnumerable> groupedCommands, { using (sb.BeginIndent($"case \"{commands.Key}\":")) { - // TODO: check leaf command - if (commands.Count() != 1) + var nextDepth = depth + 1; + var leafCommand = commands.SingleOrDefault(x => x.Command.CommandPath.Length < nextDepth); + var nextGroup = commands.Where(x => x != leafCommand).ToLookup(x => x.Command.CommandPath.Length == nextDepth ? x.Command.CommandName : x.Command.CommandPath[nextDepth]); + if (nextGroup.Count() != 0) { - // recursive: next depth - var nextDepth = depth + 1; - var nextGroup = commands.GroupBy(x => x.Command.CommandPath.Length < nextDepth ? x.Command.CommandName : x.Command.CommandPath[nextDepth]); EmitRunBody(nextGroup, nextDepth, isRunAsync); sb.AppendLine("break;"); } - else + + if (leafCommand != null) { - var cmd = commands.First(); + var cmd = leafCommand; string commandArgs = ""; if (cmd.Command.DelegateBuildType != DelegateBuildType.None) @@ -386,7 +386,7 @@ void EmitRunBody(IEnumerable> groupedCommands, } else { - sb.AppendLine($"result = RunAsyncCommand{cmd.Id}(args[1..]{commandArgs});"); + sb.AppendLine($"result = RunAsyncCommand{cmd.Id}(args[{depth + 1}..]{commandArgs});"); } sb.AppendLine("break;"); } From 303f40aeaeb2eb7d03709c234b407b8b90c1ff81 Mon Sep 17 00:00:00 2001 From: neuecc Date: Tue, 28 May 2024 17:36:14 +0900 Subject: [PATCH 29/54] done builder API --- sandbox/GeneratorSandbox/Program.cs | 142 +++++++++++++++--- .../ConsoleAppGenerator.cs | 1 + src/ConsoleAppFramework5/Emitter.cs | 94 ++++++++---- src/ConsoleAppFramework5/Parser.cs | 28 +++- .../CSharpGeneratorRunner.cs | 2 +- .../IndentStringBuilder.cs | 32 ---- .../SubCommandTest.cs | 125 +++++++++++++++ 7 files changed, 335 insertions(+), 89 deletions(-) delete mode 100644 tests/ConsoleAppFramework.GeneratorTests/IndentStringBuilder.cs create mode 100644 tests/ConsoleAppFramework.GeneratorTests/SubCommandTest.cs diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index be5c2ec..2e430c7 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -19,25 +19,78 @@ using Microsoft.Extensions.DependencyInjection; -args = ["do"]; // test. +// args = ["do"]; // test. + +var builder = ConsoleApp.CreateBuilder(); + +//builder.Add("", () => { }); +//builder.Add("a", () => { }); +//builder.Add("a/b1", () => { }); +//builder.Add("a/b2", () => { }); +//builder.Add("a/b2/c", () => { }); +//builder.Add("do", () => { }); + +//builder.Add("age"); + +//builder.Run(args); + + + + + +builder.Add("", () => { Console.Write("root"); }); + +builder.Run(args); + + + + + + + + + + + + + + + + + + + -// ConsoleApp.Run(args, Run2); void Run2(int x, int yzzzz) { }; -//ConsoleApp.Run(args, ([Range(1, 10)] int x, int y) => -//{ -//}); -var builder = ConsoleApp.CreateBuilder(); -builder.Add(); -builder.Add("foo/tako", (int x, int y) => { return "foo"; }); -// builder.Add("foo/tako/ekkusu", (int x, int y, int z) => { return "foo"; }); -await builder.RunAsync(args); + + + + + + + + + + + + + + + + + +//builder.Add("foo/tako", (int x, int y) => { return "foo"; }); +//builder.Add("foo/tako/ekkusu", (int x, int y, int z) => { return "foo"; }); + + +// builder.Run(args); // var s = "foo"; // s.AsSpan().Split(',',). @@ -94,6 +147,8 @@ void Echo() public static void Sum() { } + + } public class MyCommands : IDisposable @@ -344,15 +399,66 @@ public static bool TryParse(ReadOnlySpan s, out T[] result) namespace ConsoleAppFramework { - //partial class ConsoleApp - //{ - + partial class ConsoleApp + { + partial struct ConsoleAppBuilder + { + //void RunAsyncCore2(string[] args, ref Task result) + //{ + // if (args.Length == 0) + // { + // // invoke root command(or show help) + // return; + // } + + // switch (args[0]) + // { + // case "foo": + // if (args.Length == 1) + // { + // // invoke leaf command(or show help) + // } + + // switch (args[1]) + // { + // case "tako": + // if (args.Length == 2) + // { + // result = RunAsyncCommand0(args[1..], command0); + // return; + // } + + // switch (args[2]) + // { + // case "ekkusu": + // result = RunAsyncCommand1(args[3..], command1); + // break; + // default: + // break; + // } + // break; + // default: + // // invoke leaf command(or show help) + // break; + // } + // break; + // case "do": + // result = RunAsyncCommand2(args[1..]); + // break; + // case "sum": + // result = RunAsyncCommand3(args[1..]); + // break; + // case "echo": + // result = RunAsyncCommand4(args[1..]); + // break; + // default: + // // invoke root command(or show help) + // break; + // } + //} - // public ConsoleAppBuilder CreateBuilder() - // { - // return new ConsoleAppBuilder(); - // } - //} + } + } //public class ConsoleAppBuilder diff --git a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs index 52b6446..885a594 100644 --- a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework5/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; diff --git a/src/ConsoleAppFramework5/Emitter.cs b/src/ConsoleAppFramework5/Emitter.cs index fe5c151..e3dacc8 100644 --- a/src/ConsoleAppFramework5/Emitter.cs +++ b/src/ConsoleAppFramework5/Emitter.cs @@ -352,53 +352,87 @@ public void EmitBuilder(SourceBuilder sb, Command[] commands, bool emitSync, boo } } - void EmitRunBody(IEnumerable> groupedCommands, int depth, bool isRunAsync) + void EmitRunBody(ILookup groupedCommands, int depth, bool isRunAsync) { + var leafCommand = groupedCommands[""].FirstOrDefault(); + IDisposable? ifBlcok = null; + if (!(groupedCommands.Count == 1 && leafCommand != null)) + { + ifBlcok = sb.BeginBlock($"if (args.Length == {depth})"); + } + EmitLeafCommand(leafCommand); + if (ifBlcok != null) + { + sb.AppendLine("return;"); + ifBlcok.Dispose(); + } + else + { + return; + } + using (sb.BeginBlock($"switch (args[{depth}])")) { - // case:... - foreach (var commands in groupedCommands) + foreach (var commands in groupedCommands.Where(x => x.Key != "")) { using (sb.BeginIndent($"case \"{commands.Key}\":")) { var nextDepth = depth + 1; - var leafCommand = commands.SingleOrDefault(x => x.Command.CommandPath.Length < nextDepth); - var nextGroup = commands.Where(x => x != leafCommand).ToLookup(x => x.Command.CommandPath.Length == nextDepth ? x.Command.CommandName : x.Command.CommandPath[nextDepth]); - if (nextGroup.Count() != 0) - { - EmitRunBody(nextGroup, nextDepth, isRunAsync); - sb.AppendLine("break;"); - } - - if (leafCommand != null) - { - var cmd = leafCommand; - - string commandArgs = ""; - if (cmd.Command.DelegateBuildType != DelegateBuildType.None) + var nextGroup = commands + .ToLookup(x => { - commandArgs = $", command{cmd.Id}"; - } + var len = x.Command.CommandPath.Length; + if (len > nextDepth) + { + return x.Command.CommandPath[nextDepth]; + } + if (len == nextDepth) + { + return x.Command.CommandName; + } + else + { + return ""; // as leaf command + } + }); - if (!isRunAsync) - { - sb.AppendLine($"RunCommand{cmd.Id}(args.AsSpan({depth + 1}){commandArgs});"); - } - else - { - sb.AppendLine($"result = RunAsyncCommand{cmd.Id}(args[{depth + 1}..]{commandArgs});"); - } - sb.AppendLine("break;"); - } + EmitRunBody(nextGroup, nextDepth, isRunAsync); // recursive + sb.AppendLine("break;"); } } - // TODO: invoke root command using (sb.BeginIndent("default:")) { + var leafCommand2 = groupedCommands[""].FirstOrDefault(); + EmitLeafCommand(leafCommand2); sb.AppendLine("break;"); } } + + void EmitLeafCommand(CommandWithId? command) + { + if (command == null) + { + sb.AppendLine("TryShowHelpOrVersion(args, -1);"); + } + else + { + string commandArgs = ""; + if (command.Command.DelegateBuildType != DelegateBuildType.None) + { + commandArgs = $", command{command.Id}"; + } + + if (!isRunAsync) + { + sb.AppendLine($"RunCommand{command.Id}(args.AsSpan({depth}){commandArgs});"); + } + else + { + sb.AppendLine($"result = RunAsyncCommand{command.Id}(args[{depth}..]{commandArgs});"); + } + } + } } } diff --git a/src/ConsoleAppFramework5/Parser.cs b/src/ConsoleAppFramework5/Parser.cs index b2da7ef..3bca9cd 100644 --- a/src/ConsoleAppFramework5/Parser.cs +++ b/src/ConsoleAppFramework5/Parser.cs @@ -1,8 +1,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -using System.Linq; -using System.Reflection.Metadata; namespace ConsoleAppFramework; @@ -65,7 +63,21 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta var genericName = (node.Expression as MemberAccessExpressionSyntax)?.Name as GenericNameSyntax; var genericType = genericName!.TypeArgumentList.Arguments[0]; - // TODO: Add(string commandPath) + // Add(string commandPath) + string[] commandPath = []; + var args = node.ArgumentList.Arguments; + if (node.ArgumentList.Arguments.Count == 1) + { + var commandName = args[0]; + if (!commandName.Expression.IsKind(SyntaxKind.StringLiteralExpression)) + { + context.ReportDiagnostic(DiagnosticDescriptors.AddCommandMustBeStringLiteral, commandName.GetLocation()); + return []; + } + + var name = (commandName.Expression as LiteralExpressionSyntax)!.Token.ValueText; + commandPath = name.Split(['/'], StringSplitOptions.RemoveEmptyEntries); + } // T var type = model.GetTypeInfo(genericType).Type!; @@ -125,7 +137,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta commandName = x.Name.ToLowerInvariant(); } - var command = ParseFromMethodSymbol(x, false, commandName); + var command = ParseFromMethodSymbol(x, false, commandPath, commandName); if (command == null) return null; command.CommandMethodInfo = methodInfoBase with { MethodName = x.Name }; @@ -146,7 +158,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta var methodSymbols = model.GetMemberGroup(operand); if (methodSymbols.Length > 0 && methodSymbols[0] is IMethodSymbol methodSymbol) { - return ParseFromMethodSymbol(methodSymbol, addressOf: true, commandName); + return ParseFromMethodSymbol(methodSymbol, addressOf: true, commandPath, commandName); } } else @@ -154,7 +166,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta var methodSymbols = model.GetMemberGroup(expression); if (methodSymbols.Length > 0 && methodSymbols[0] is IMethodSymbol methodSymbol) { - return ParseFromMethodSymbol(methodSymbol, addressOf: false, commandName); + return ParseFromMethodSymbol(methodSymbol, addressOf: false, commandPath, commandName); } } } @@ -345,7 +357,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta return cmd; } - Command? ParseFromMethodSymbol(IMethodSymbol methodSymbol, bool addressOf, string commandName) + Command? ParseFromMethodSymbol(IMethodSymbol methodSymbol, bool addressOf, string[] commandPath, string commandName) { var docComment = methodSymbol.DeclaringSyntaxReferences[0].GetSyntax().GetDocumentationCommentTriviaSyntax(); var summary = ""; @@ -462,7 +474,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta var cmd = new Command { CommandName = commandName, - CommandPath = [], + CommandPath = commandPath, IsAsync = isAsync, IsVoid = isVoid, Parameters = parameters, diff --git a/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs b/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs index 93cac87..25f4f0c 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs @@ -139,7 +139,7 @@ public void Execute(string code, string args, string expected, [CallerArgumentEx { output.WriteLine(codeExpr); - var (compilation, diagnostics, stdout) = CSharpGeneratorRunner.CompileAndExecute(code, args.Split(' ')); + var (compilation, diagnostics, stdout) = CSharpGeneratorRunner.CompileAndExecute(code, args == "" ? [] : args.Split(' ')); foreach (var item in diagnostics) { output.WriteLine(item.ToString()); diff --git a/tests/ConsoleAppFramework.GeneratorTests/IndentStringBuilder.cs b/tests/ConsoleAppFramework.GeneratorTests/IndentStringBuilder.cs deleted file mode 100644 index 1e4515b..0000000 --- a/tests/ConsoleAppFramework.GeneratorTests/IndentStringBuilder.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Text; - -namespace ConsoleAppFramework.GeneratorTests; - -public class IndentStringBuilder(int level) -{ - StringBuilder builder = new StringBuilder(); - - public void Indent() - { - level++; - } - - public void Unindent() - { - level--; - } - - public void AppendLine(string text) - { - if (level != 0) - { - builder.Append(' ', 4 * level); - } - builder.AppendLine(text); - } - - public override string ToString() - { - return builder.ToString(); - } -} diff --git a/tests/ConsoleAppFramework.GeneratorTests/SubCommandTest.cs b/tests/ConsoleAppFramework.GeneratorTests/SubCommandTest.cs new file mode 100644 index 0000000..dbe3ef2 --- /dev/null +++ b/tests/ConsoleAppFramework.GeneratorTests/SubCommandTest.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit.Abstractions; + +namespace ConsoleAppFramework.GeneratorTests; + +public class SubCommandTest(ITestOutputHelper output) +{ + VerifyHelper verifier = new VerifyHelper(output, "CAF"); + + [Fact] + public void Zeroargs() + { + var code = """ +var builder = ConsoleApp.CreateBuilder(); + +builder.Add("", () => { Console.Write("root"); }); +builder.Add("a", () => { Console.Write("a"); }); +builder.Add("a/b1", () => { Console.Write("a/b1"); }); +builder.Add("a/b2", () => { Console.Write("a/b2"); }); +builder.Add("a/b2/c", () => { Console.Write("a/b2/c"); }); +builder.Add("a/b2/d", () => { Console.Write("a/b2/d"); }); +builder.Add("a/b2/d/e", () => { Console.Write("a/b2/d/e"); }); +builder.Add("a/b/c/d/e/f", () => { Console.Write("a/b/c/d/e/f"); }); + +builder.Run(args); +"""; + + verifier.Execute(code, "", "root"); // root + verifier.Execute(code, "a", "a"); + verifier.Execute(code, "a b1", "a/b1"); + verifier.Execute(code, "a b2", "a/b2"); + verifier.Execute(code, "a b2 c", "a/b2/c"); + verifier.Execute(code, "a b2 d", "a/b2/d"); + verifier.Execute(code, "a b2 d e", "a/b2/d/e"); + verifier.Execute(code, "a b c d e f", "a/b/c/d/e/f"); + } + + [Fact] + public void Withargs() + { + var code = """ +var builder = ConsoleApp.CreateBuilder(); + +builder.Add("", (int x, int y) => { Console.Write($"root {x} {y}"); }); +builder.Add("a", (int x, int y) => { Console.Write($"a {x} {y}"); }); +builder.Add("a/b1", (int x, int y) => { Console.Write($"a/b1 {x} {y}"); }); +builder.Add("a/b2", (int x, int y) => { Console.Write($"a/b2 {x} {y}"); }); +builder.Add("a/b2/c", (int x, int y) => { Console.Write($"a/b2/c {x} {y}"); }); +builder.Add("a/b2/d", (int x, int y) => { Console.Write($"a/b2/d {x} {y}"); }); +builder.Add("a/b2/d/e", (int x, int y) => { Console.Write($"a/b2/d/e {x} {y}"); }); +builder.Add("a/b/c/d/e/f", (int x, int y) => { Console.Write($"a/b/c/d/e/f {x} {y}"); }); + +builder.Run(args); +"""; + + verifier.Execute(code, "--x 10 --y 20", "root 10 20"); // root + verifier.Execute(code, "a --x 10 --y 20", "a 10 20"); + verifier.Execute(code, "a b1 --x 10 --y 20", "a/b1 10 20"); + verifier.Execute(code, "a b2 --x 10 --y 20", "a/b2 10 20"); + verifier.Execute(code, "a b2 c --x 10 --y 20", "a/b2/c 10 20"); + verifier.Execute(code, "a b2 d --x 10 --y 20", "a/b2/d 10 20"); + verifier.Execute(code, "a b2 d e --x 10 --y 20", "a/b2/d/e 10 20"); + verifier.Execute(code, "a b c d e f --x 10 --y 20", "a/b/c/d/e/f 10 20"); + } + + [Fact] + public void ZeroargsAsync() + { + var code = """ +var builder = ConsoleApp.CreateBuilder(); + +builder.Add("", () => { Console.Write("root"); }); +builder.Add("a", () => { Console.Write("a"); }); +builder.Add("a/b1", () => { Console.Write("a/b1"); }); +builder.Add("a/b2", () => { Console.Write("a/b2"); }); +builder.Add("a/b2/c", () => { Console.Write("a/b2/c"); }); +builder.Add("a/b2/d", () => { Console.Write("a/b2/d"); }); +builder.Add("a/b2/d/e", () => { Console.Write("a/b2/d/e"); }); +builder.Add("a/b/c/d/e/f", () => { Console.Write("a/b/c/d/e/f"); }); + +await builder.RunAsync(args); +"""; + + verifier.Execute(code, "", "root"); // root + verifier.Execute(code, "a", "a"); + verifier.Execute(code, "a b1", "a/b1"); + verifier.Execute(code, "a b2", "a/b2"); + verifier.Execute(code, "a b2 c", "a/b2/c"); + verifier.Execute(code, "a b2 d", "a/b2/d"); + verifier.Execute(code, "a b2 d e", "a/b2/d/e"); + verifier.Execute(code, "a b c d e f", "a/b/c/d/e/f"); + } + + [Fact] + public void WithargsAsync() + { + var code = """ +var builder = ConsoleApp.CreateBuilder(); + +builder.Add("", (int x, int y) => { Console.Write($"root {x} {y}"); }); +builder.Add("a", (int x, int y) => { Console.Write($"a {x} {y}"); }); +builder.Add("a/b1", (int x, int y) => { Console.Write($"a/b1 {x} {y}"); }); +builder.Add("a/b2", (int x, int y) => { Console.Write($"a/b2 {x} {y}"); }); +builder.Add("a/b2/c", (int x, int y) => { Console.Write($"a/b2/c {x} {y}"); }); +builder.Add("a/b2/d", (int x, int y) => { Console.Write($"a/b2/d {x} {y}"); }); +builder.Add("a/b2/d/e", (int x, int y) => { Console.Write($"a/b2/d/e {x} {y}"); }); +builder.Add("a/b/c/d/e/f", (int x, int y) => { Console.Write($"a/b/c/d/e/f {x} {y}"); }); + +await builder.RunAsync(args); +"""; + + verifier.Execute(code, "--x 10 --y 20", "root 10 20"); // root + verifier.Execute(code, "a --x 10 --y 20", "a 10 20"); + verifier.Execute(code, "a b1 --x 10 --y 20", "a/b1 10 20"); + verifier.Execute(code, "a b2 --x 10 --y 20", "a/b2 10 20"); + verifier.Execute(code, "a b2 c --x 10 --y 20", "a/b2/c 10 20"); + verifier.Execute(code, "a b2 d --x 10 --y 20", "a/b2/d 10 20"); + verifier.Execute(code, "a b2 d e --x 10 --y 20", "a/b2/d/e 10 20"); + verifier.Execute(code, "a b c d e f --x 10 --y 20", "a/b/c/d/e/f 10 20"); + } +} From 65c491d2c62566b6f2f91fc2d04e63200e30d48f Mon Sep 17 00:00:00 2001 From: neuecc Date: Tue, 28 May 2024 17:45:10 +0900 Subject: [PATCH 30/54] prepare for filter --- sandbox/GeneratorSandbox/Program.cs | 2 +- .../ConsoleAppGenerator.cs | 44 +++++++++++++++++++ .../ConsoleAppBuilderTest.cs | 2 +- 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index 2e430c7..cdb93d4 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -38,7 +38,7 @@ -builder.Add("", () => { Console.Write("root"); }); +builder.Add("", (CancellationToken ct) => { Console.Write("root"); }); builder.Run(args); diff --git a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs index 885a594..8fa45c8 100644 --- a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs @@ -114,6 +114,19 @@ public CommandAttribute(string command) } } +internal abstract class ConsoleAppFilter(ConsoleAppFilter next) +{ + protected ConsoleAppFilter Next = next; + + public abstract Task InvokeAsync(CancellationToken cancellationToken); +} + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = false)] +internal sealed class ConsoleAppFilterAttribute : Attribute + where T : ConsoleAppFilter +{ +} + internal static partial class ConsoleApp { public static IServiceProvider? ServiceProvider { get; set; } @@ -288,6 +301,37 @@ static void ShowHelp() Log("TODO: Build Help"); } + static async Task RunWithFilterAsync(ConsoleAppFilter invoker) + { + using var posixSignalHandler = PosixSignalHandler.Register(Timeout); + try + { + await Task.Run(() => invoker.InvokeAsync(posixSignalHandler.Token)).WaitAsync(posixSignalHandler.TimeoutToken); + } + catch (OperationCanceledException ex) when (ex.CancellationToken == posixSignalHandler.Token || ex.CancellationToken == posixSignalHandler.TimeoutToken) + { + Environment.ExitCode = 130; + } + catch (Exception ex) + { + if ((ex is OperationCanceledException oce) && (oce.CancellationToken == posixSignalHandler.Token || oce.CancellationToken == posixSignalHandler.TimeoutToken)) + { + Environment.ExitCode = 130; + return; + } + + Environment.ExitCode = 1; + if (ex is ValidationException) + { + LogError(ex.Message); + } + else + { + LogError(ex.ToString()); + } + } + } + sealed class PosixSignalHandler : IDisposable { public CancellationToken Token => cancellationTokenSource.Token; diff --git a/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppBuilderTest.cs b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppBuilderTest.cs index 084b3b0..d5cf2ed 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppBuilderTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppBuilderTest.cs @@ -201,7 +201,7 @@ public object GetService(Type serviceType) } [Fact] - public void Command() + public void CommandAttr() { var code = """ var builder = ConsoleApp.CreateBuilder(); From e4c6966c5626a6855b514f94c9de1559bc779151 Mon Sep 17 00:00:00 2001 From: neuecc Date: Tue, 28 May 2024 21:07:11 +0900 Subject: [PATCH 31/54] filter for delegate --- sandbox/GeneratorSandbox/Program.cs | 405 ++++++------------ src/ConsoleAppFramework5/Command.cs | 26 ++ .../ConsoleAppGenerator.cs | 55 ++- src/ConsoleAppFramework5/Emitter.cs | 128 ++++-- src/ConsoleAppFramework5/Parser.cs | 12 +- src/ConsoleAppFramework5/SourceBuilder.cs | 11 + 6 files changed, 319 insertions(+), 318 deletions(-) diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index cdb93d4..abe3ce5 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -17,6 +17,7 @@ using System.Threading.Tasks; using ConsoleAppFramework; using Microsoft.Extensions.DependencyInjection; +using static ConsoleAppFramework.ConsoleApp; // args = ["do"]; // test. @@ -35,10 +36,11 @@ //builder.Run(args); +builder.AddFilter(); +builder.AddFilter(); - -builder.Add("", (CancellationToken ct) => { Console.Write("root"); }); +builder.Add("", (CancellationToken ct) => { Console.WriteLine("body"); }); builder.Run(args); @@ -401,333 +403,180 @@ namespace ConsoleAppFramework { partial class ConsoleApp { - partial struct ConsoleAppBuilder - { - //void RunAsyncCore2(string[] args, ref Task result) - //{ - // if (args.Length == 0) - // { - // // invoke root command(or show help) - // return; - // } - // switch (args[0]) - // { - // case "foo": - // if (args.Length == 1) - // { - // // invoke leaf command(or show help) - // } - - // switch (args[1]) - // { - // case "tako": - // if (args.Length == 2) - // { - // result = RunAsyncCommand0(args[1..], command0); - // return; - // } - - // switch (args[2]) - // { - // case "ekkusu": - // result = RunAsyncCommand1(args[3..], command1); - // break; - // default: - // break; - // } - // break; - // default: - // // invoke leaf command(or show help) - // break; - // } - // break; - // case "do": - // result = RunAsyncCommand2(args[1..]); - // break; - // case "sum": - // result = RunAsyncCommand3(args[1..]); - // break; - // case "echo": - // result = RunAsyncCommand4(args[1..]); - // break; - // default: - // // invoke root command(or show help) - // break; - // } - //} - } - } + partial struct ConsoleAppBuilder + { - //public class ConsoleAppBuilder - //{ - // public void Add() - // { - // } + // public void AddFilter() where T : ConsoleAppFilter { } + //public class ConsoleAppBuilder + //{ + // public void Add() + // { + // } - // public void Run(string[] args) - // { - // if (args.Length == 0 || args[0].StartsWith('-')) - // { - // // invoke root command - // } - // } - // public void RunAsync(string[] args) - // { - // } - //} - partial class ConsoleApp - { - private static async Task RunAsyncCommand1(string[] args) - { - if (TryShowHelpOrVersion(args, 0)) return; + // public void Run(string[] args) + // { + // if (args.Length == 0 || args[0].StartsWith('-')) + // { + // // invoke root command + // } + // } - using var posixSignalHandler = PosixSignalHandler.Register(Timeout); - var arg0 = posixSignalHandler.Token; + // public void RunAsync(string[] args) + // { + // } + //} - try + partial class ConsoleApp { - for (int i = 0; i < args.Length; i++) + private static async Task RunAsyncCommand1(string[] args) { - var name = args[i]; + if (TryShowHelpOrVersion(args, 0)) return; + + using var posixSignalHandler = PosixSignalHandler.Register(Timeout); + var arg0 = posixSignalHandler.Token; - switch (name) + try { - default: - ThrowArgumentNameNotFound(name); - break; + for (int i = 0; i < args.Length; i++) + { + var name = args[i]; + + switch (name) + { + default: + ThrowArgumentNameNotFound(name); + break; + } + } + var instance = new global::MyClass(); + await Task.Run(() => instance.Do(arg0!)).WaitAsync(posixSignalHandler.TimeoutToken); } - } - var instance = new global::MyClass(); - await Task.Run(() => instance.Do(arg0!)).WaitAsync(posixSignalHandler.TimeoutToken); - } - catch (Exception ex) - { - if ((ex is OperationCanceledException oce) && (oce.CancellationToken == posixSignalHandler.Token || oce.CancellationToken == posixSignalHandler.TimeoutToken)) - { - Environment.ExitCode = 130; - return; - } + catch (Exception ex) + { + if ((ex is OperationCanceledException oce) && (oce.CancellationToken == posixSignalHandler.Token || oce.CancellationToken == posixSignalHandler.TimeoutToken)) + { + Environment.ExitCode = 130; + return; + } - Environment.ExitCode = 1; - if (ex is System.ComponentModel.DataAnnotations.ValidationException) - { - LogError(ex.Message); + Environment.ExitCode = 1; + if (ex is System.ComponentModel.DataAnnotations.ValidationException) + { + LogError(ex.Message); + } + else + { + LogError(ex.ToString()); + } + } } - else + + + + public struct Builder() { - LogError(ex.ToString()); + } } } - public struct Builder() + public class FilterContext : IServiceProvider { - private static void RunCommand0(ReadOnlySpan args) - { - // if (TryShowHelpOrVersion(args, 0)) return; - - - try - { - for (int i = 0; i < args.Length; i++) - { - - var name = args[i]; - - switch (name) - { + public long Timestamp { get; set; } + public Guid UserId { get; set; } - default: + object IServiceProvider.GetService(Type serviceType) + { + if (serviceType == typeof(FilterContext)) return this; + throw new InvalidOperationException("Type is invalid:" + serviceType); + } + } - ThrowArgumentNameNotFound(name); - break; - } - } + //public abstract class ConsoleAppFilter(ConsoleAppFilter next) + //{ + // protected ConsoleAppFilter Next = next; + // public abstract ValueTask InvokeAsync(CancellationToken cancellationToken); + //} - var instance = new global::MyClass(); - // instance.Do(); - } + //[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = false)] + //public sealed class ConsoleAppFilterAttribute : Attribute + // where T : ConsoleAppFilter + //{ + //} - catch (Exception ex) - { - Environment.ExitCode = 1; - if (ex is System.ComponentModel.DataAnnotations.ValidationException) - { - LogError(ex.Message); - } - else - { - LogError(ex.ToString()); - } - } - } - - public void RunCore2(string[] args) + public class TimestampFilter(ConsoleAppFilter next) + : ConsoleAppFilter(next) + { + public override Task InvokeAsync(CancellationToken cancellationToken) { - switch (args[0]) - { - case "do": - RunWithFilterAsync(new Command0Invoker(args[1..]).BuildFilter()).GetAwaiter().GetResult(); - break; - case "tako": - switch (args[1]) // incr... - { - case "foo": - break; - default: - break; - } - break; - //case "tako": - //break; - default: - break; - } + Console.WriteLine("filter1"); + return Next.InvokeAsync(cancellationToken); } + } - // move to ConsoleApp template? - static async Task RunWithFilterAsync(ConsoleAppFilter invoker) + + public class LogExecutionTimeFilter(ConsoleAppFilter next) + : ConsoleAppFilter(next) + { + public override async Task InvokeAsync(CancellationToken cancellationToken) { - using var posixSignalHandler = PosixSignalHandler.Register(Timeout); + Console.WriteLine("filter2"); - // in core, remove try-catch...? - try - { - await Task.Run(() => invoker.InvokeAsync(posixSignalHandler.Token).AsTask()).WaitAsync(posixSignalHandler.TimeoutToken); - } - catch (OperationCanceledException ex) when (ex.CancellationToken == posixSignalHandler.Token || ex.CancellationToken == posixSignalHandler.TimeoutToken) - { - Environment.ExitCode = 130; - } - catch (Exception ex) - { - Environment.ExitCode = 1; - if (ex is System.ComponentModel.DataAnnotations.ValidationException) - { - LogError(ex.Message); - } - else - { - LogError(ex.ToString()); - } - } - } - sealed class Command0Invoker(string[] args) : ConsoleAppFilter(null!) - { - public ConsoleAppFilter BuildFilter() + var startingTime = Stopwatch.GetTimestamp(); + try { - var f3 = new TimestampFilter(this); // and DI. - var f2 = new TimestampFilter(f3); - var f1 = new TimestampFilter(f2); - - return f1; + await Next.InvokeAsync(cancellationToken); } - - public override ValueTask InvokeAsync(CancellationToken cancellationToken) + finally { - RunCommand0(args); // pass: cancellationToken. - return default; + var elapsed = Stopwatch.GetElapsedTime(startingTime); + ConsoleApp.Log($"Execution Time: {elapsed.ToString()}"); } } } - } -} - - - -public class FilterContext : IServiceProvider -{ - public long Timestamp { get; set; } - public Guid UserId { get; set; } - object IServiceProvider.GetService(Type serviceType) - { - if (serviceType == typeof(FilterContext)) return this; - throw new InvalidOperationException("Type is invalid:" + serviceType); - } -} - -public abstract class ConsoleAppFilter(ConsoleAppFilter next) -{ - protected ConsoleAppFilter Next = next; - public abstract ValueTask InvokeAsync(CancellationToken cancellationToken); -} - -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = false)] -public sealed class ConsoleAppFilterAttribute : Attribute - where T : ConsoleAppFilter -{ -} -public class TimestampFilter(ConsoleAppFilter next) - : ConsoleAppFilter(next) -{ - public override ValueTask InvokeAsync(CancellationToken cancellationToken) - { - return Next.InvokeAsync(cancellationToken); - } -} - - -public class LogExecutionTimeFilter(ConsoleAppFilter next) - : ConsoleAppFilter(next) -{ - public override async ValueTask InvokeAsync(CancellationToken cancellationToken) - { - var startingTime = Stopwatch.GetTimestamp(); - try + public class MyContext : IServiceProvider { - await Next.InvokeAsync(cancellationToken); + public long Timestamp { get; set; } + public Guid UserId { get; set; } + + object IServiceProvider.GetService(Type serviceType) + { + if (serviceType == typeof(MyContext)) return this; + throw new InvalidOperationException("Type is invalid:" + serviceType); + } } - finally + + public class MyClass23 { - var elapsed = Stopwatch.GetElapsedTime(startingTime); - ConsoleApp.Log($"Execution Time: {elapsed.ToString()}"); + public void Do() + { + Console.Write("yeah:"); + } } - } -} - - -public class MyContext : IServiceProvider -{ - public long Timestamp { get; set; } - public Guid UserId { get; set; } - - object IServiceProvider.GetService(Type serviceType) - { - if (serviceType == typeof(MyContext)) return this; - throw new InvalidOperationException("Type is invalid:" + serviceType); - } -} - -public class MyClass23 -{ - public void Do() - { - Console.Write("yeah:"); - } -} - -[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -internal sealed class CommandAttribute : Attribute -{ - public string Command { get; } + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] + internal sealed class CommandAttribute : Attribute + { + public string Command { get; } - public CommandAttribute(string command) - { - this.Command = command; + public CommandAttribute(string command) + { + this.Command = command; + } + } } } \ No newline at end of file diff --git a/src/ConsoleAppFramework5/Command.cs b/src/ConsoleAppFramework5/Command.cs index e6e641d..c11a564 100644 --- a/src/ConsoleAppFramework5/Command.cs +++ b/src/ConsoleAppFramework5/Command.cs @@ -32,6 +32,8 @@ public record class Command public required MethodKind MethodKind { get; init; } public required DelegateBuildType DelegateBuildType { get; init; } public CommandMethodInfo? CommandMethodInfo { get; set; } // can set...! + public required FilterInfo[] Filters { get; init; } + public bool HasFilter => Filters.Length != 0; public string? BuildDelegateSignature(out string? delegateType) { @@ -318,6 +320,30 @@ public string BuildNew() return $"({type})ServiceProvider!.GetService(typeof({type}))!"; }); + return $"new {TypeFullName}({string.Join(", ", p)})"; + } +} + +public record class FilterInfo +{ + public required string TypeFullName { get; init; } + public required ITypeSymbol[] ConstructorParameterTypes { get; init; } + + public string BuildNew(string nextFilterName) + { + var p = ConstructorParameterTypes.Select(parameter => + { + var type = parameter.ToFullyQualifiedFormatDisplayString(); + if (type.Contains("ConsoleAppFramework.ConsoleAppFilter")) + { + return nextFilterName; + } + else + { + return $"({type})ServiceProvider!.GetService(typeof({type}))!"; + } + }); + return $"new {TypeFullName}({string.Join(", ", p)})"; } } \ No newline at end of file diff --git a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs index 8fa45c8..b32ad8a 100644 --- a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs @@ -4,6 +4,7 @@ using System.Collections.Immutable; using System.ComponentModel.Design; using System.Reflection; +using System.Xml.Linq; namespace ConsoleAppFramework; @@ -51,7 +52,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var expr = invocationExpression.Expression as MemberAccessExpressionSyntax; var methodName = expr?.Name.Identifier.Text; - if (methodName is "Add" or "Run" or "RunAsync") + if (methodName is "Add" or "AddFilter" or "Run" or "RunAsync") { return true; } @@ -398,6 +399,9 @@ public void Add() { } [System.Diagnostics.Conditional("DEBUG")] public void Add(string commandPath) { } + [System.Diagnostics.Conditional("DEBUG")] + public void AddFilter() where T : ConsoleAppFilter { } + public void Run(string[] args) { RunCore(args); @@ -467,7 +471,7 @@ static void EmitConsoleAppRun(SourceProductionContext sourceProductionContext, ( var wellKnownTypes = new WellKnownTypes(model.Compilation); - var parser = new Parser(sourceProductionContext, node, model, wellKnownTypes, DelegateBuildType.MakeDelegateWhenHasDefaultValue); + var parser = new Parser(sourceProductionContext, node, model, wellKnownTypes, DelegateBuildType.MakeDelegateWhenHasDefaultValue, []); var command = parser.ParseAndValidate(); if (command == null) { @@ -510,7 +514,7 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex } } - var group1 = generatorSyntaxContexts.ToLookup(x => + var methodGroup = generatorSyntaxContexts.ToLookup(x => { if (x.Name == "Add" && ((x.Node.Expression as MemberAccessExpressionSyntax)?.Name.IsKind(SyntaxKind.GenericName) ?? false)) { @@ -520,12 +524,41 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex return x.Name; }); + var globalFilters = methodGroup["AddFilter"] + .Select(x => + { + var genericName = (x.Node.Expression as MemberAccessExpressionSyntax)?.Name as GenericNameSyntax; + var genericType = genericName!.TypeArgumentList.Arguments[0]; + var type = model.GetTypeInfo(genericType).Type; + if (type == null) return null!; + + var publicConstructors = type.GetMembers() + .OfType() + .Where(x => x.MethodKind == Microsoft.CodeAnalysis.MethodKind.Constructor && x.DeclaredAccessibility == Accessibility.Public) + .ToArray(); + + if (publicConstructors.Length != 1) + { + // TODO: validation + } + + var filter = new FilterInfo + { + TypeFullName = type.ToFullyQualifiedFormatDisplayString(), + ConstructorParameterTypes = publicConstructors[0].Parameters.Select(x => x.Type).ToArray() + }; + + return filter; + }) + .Where(x => x != null) + .ToArray(); + var names = new HashSet(); - var commands1 = group1["Add"] + var commands1 = methodGroup["Add"] .Select(x => { - var parser = new Parser(sourceProductionContext, x.Node, x.Model, wellKnownTypes, DelegateBuildType.OnlyActionFunc); - var command = parser.ParseAndValidateForCommand(); + var parser = new Parser(sourceProductionContext, x.Node, x.Model, wellKnownTypes, DelegateBuildType.OnlyActionFunc, globalFilters); + var command = parser.ParseAndValidateForBuilderDelegateRegistration(); // validation command name duplicate if (command != null && !names.Add(command.CommandFullName)) @@ -538,11 +571,11 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex }) .ToArray(); // evaluate first. - var commands2 = group1["Add"] + var commands2 = methodGroup["Add"] .SelectMany(x => { - var parser = new Parser(sourceProductionContext, x.Node, x.Model, wellKnownTypes, DelegateBuildType.None); - var commands = parser.ParseForBuilderClassRegistration(); + var parser = new Parser(sourceProductionContext, x.Node, x.Model, wellKnownTypes, DelegateBuildType.None, globalFilters); + var commands = parser.ParseAndValidateForBuilderClassRegistration(); // validation command name duplicate? foreach (var command in commands) @@ -567,8 +600,8 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex if (commands.Length == 0) return; - var hasRun = group1["Run"].Any(); - var hasRunAsync = group1["RunAsync"].Any(); + var hasRun = methodGroup["Run"].Any(); + var hasRunAsync = methodGroup["RunAsync"].Any(); if (!hasRun && !hasRunAsync) return; diff --git a/src/ConsoleAppFramework5/Emitter.cs b/src/ConsoleAppFramework5/Emitter.cs index e3dacc8..ed287de 100644 --- a/src/ConsoleAppFramework5/Emitter.cs +++ b/src/ConsoleAppFramework5/Emitter.cs @@ -12,6 +12,11 @@ public void EmitRun(SourceBuilder sb, Command command, bool isRunAsync, string? var hasValidation = command.Parameters.Any(x => x.HasValidation); var parsableParameterCount = command.Parameters.Count(x => x.IsParsable); + if (command.HasFilter) + { + isRunAsync = true; + hasCancellationToken = false; + } var returnType = isRunAsync ? "async Task" : "void"; var accessibility = !emitForBuilder ? "public" : "private"; var argsType = !emitForBuilder ? "string[]" : (isRunAsync ? "string[]" : "ReadOnlySpan"); // NOTE: C# 13 will allow Span in async methods so can change to ReadOnlyMemory(and store .Span in local var) @@ -31,8 +36,10 @@ public void EmitRun(SourceBuilder sb, Command command, bool isRunAsync, string? sb.AppendLine(); } + var filterCancellationToken = command.HasFilter ? ", CancellationToken cancellationToken" : ""; + // method signature - using (sb.BeginBlock($"{accessibility} static {unsafeCode}{returnType} {methodName}({argsType} args{commandMethodType})")) + using (sb.BeginBlock($"{accessibility} static {unsafeCode}{returnType} {methodName}({argsType} args{commandMethodType}{filterCancellationToken})")) { sb.AppendLine($"if (TryShowHelpOrVersion(args, {parsableParameterCount})) return;"); sb.AppendLine(); @@ -56,7 +63,14 @@ public void EmitRun(SourceBuilder sb, Command command, bool isRunAsync, string? } else if (parameter.IsCancellationToken) { - sb.AppendLine($"var arg{i} = posixSignalHandler.Token;"); + if (command.HasFilter) + { + sb.AppendLine($"var arg{i} = cancellationToken;"); + } + else + { + sb.AppendLine($"var arg{i} = posixSignalHandler.Token;"); + } } else if (parameter.IsFromServices) { @@ -66,8 +80,7 @@ public void EmitRun(SourceBuilder sb, Command command, bool isRunAsync, string? } sb.AppendLineIfExists(command.Parameters); - // try TODO: if using filter, does not emit try - using (sb.BeginBlock("try")) + using (command.HasFilter ? sb.Nop : sb.BeginBlock("try")) { using (sb.BeginBlock("for (int i = 0; i < args.Length; i++)")) { @@ -239,27 +252,31 @@ public void EmitRun(SourceBuilder sb, Command command, bool isRunAsync, string? sb.AppendLine($"Environment.ExitCode = {invokeCommand};"); } } - using (sb.BeginBlock("catch (Exception ex)")) + + if (!command.HasFilter) { - if (hasCancellationToken) + using (sb.BeginBlock("catch (Exception ex)")) { - using (sb.BeginBlock("if ((ex is OperationCanceledException oce) && (oce.CancellationToken == posixSignalHandler.Token || oce.CancellationToken == posixSignalHandler.TimeoutToken))")) + if (hasCancellationToken) { - sb.AppendLine("Environment.ExitCode = 130;"); - sb.AppendLine("return;"); + using (sb.BeginBlock("if ((ex is OperationCanceledException oce) && (oce.CancellationToken == posixSignalHandler.Token || oce.CancellationToken == posixSignalHandler.TimeoutToken))")) + { + sb.AppendLine("Environment.ExitCode = 130;"); + sb.AppendLine("return;"); + } + sb.AppendLine(); } - sb.AppendLine(); - } - sb.AppendLine("Environment.ExitCode = 1;"); + sb.AppendLine("Environment.ExitCode = 1;"); - using (sb.BeginBlock("if (ex is ValidationException)")) - { - sb.AppendLine("LogError(ex.Message);"); - } - using (sb.BeginBlock("else")) - { - sb.AppendLine("LogError(ex.ToString());"); + using (sb.BeginBlock("if (ex is ValidationException)")) + { + sb.AppendLine("LogError(ex.Message);"); + } + using (sb.BeginBlock("else")) + { + sb.AppendLine("LogError(ex.ToString());"); + } } } } @@ -332,12 +349,22 @@ public void EmitBuilder(SourceBuilder sb, Command[] commands, bool emitSync, boo } // static sync command function + HashSet emittedCommand = new(); if (emitSync) { sb.AppendLine(); foreach (var item in commandIds) { - EmitRun(sb, item.Command, false, $"RunCommand{item.Id}"); + if (!emittedCommand.Add(item.Command)) continue; + + if (item.Command.HasFilter) + { + EmitRun(sb, item.Command, true, $"RunCommand{item.Id}Async"); + } + else + { + EmitRun(sb, item.Command, false, $"RunCommand{item.Id}"); + } } } @@ -347,7 +374,18 @@ public void EmitBuilder(SourceBuilder sb, Command[] commands, bool emitSync, boo sb.AppendLine(); foreach (var item in commandIds) { - EmitRun(sb, item.Command, true, $"RunAsyncCommand{item.Id}"); + if (!emittedCommand.Add(item.Command)) continue; + EmitRun(sb, item.Command, true, $"RunCommand{item.Id}Async"); + } + } + + // filter invoker + foreach (var item in commandIds) + { + if (item.Command.HasFilter) + { + sb.AppendLine(); + EmitFilterInvoker(item); } } } @@ -423,17 +461,59 @@ void EmitLeafCommand(CommandWithId? command) commandArgs = $", command{command.Id}"; } - if (!isRunAsync) + if (!command.Command.HasFilter) { - sb.AppendLine($"RunCommand{command.Id}(args.AsSpan({depth}){commandArgs});"); + if (!isRunAsync) + { + sb.AppendLine($"RunCommand{command.Id}(args.AsSpan({depth}){commandArgs});"); + } + else + { + sb.AppendLine($"result = RunCommand{command.Id}Async(args[{depth}..]{commandArgs});"); + } } else { - sb.AppendLine($"result = RunAsyncCommand{command.Id}(args[{depth}..]{commandArgs});"); + var invokeCode = $"RunWithFilterAsync(new Command{command.Id}Invoker(args[{depth}..]{commandArgs}).BuildFilter())"; + if (!isRunAsync) + { + sb.AppendLine($"{invokeCode}.GetAwaiter().GetResult();"); + } + else + { + sb.AppendLine($"result = {invokeCode};"); + } } } } } + + void EmitFilterInvoker(CommandWithId command) + { + var commandType = command.Command.BuildDelegateSignature(out _); + var needsCommand = commandType != null; + if (needsCommand) commandType = $", {commandType} command"; + + using (sb.BeginBlock($"sealed class Command{command.Id}Invoker(string[] args{commandType}) : ConsoleAppFilter(null!)")) + { + using (sb.BeginBlock($"public ConsoleAppFilter BuildFilter()")) + { + var i = -1; + foreach (var filter in command.Command.Filters.Reverse()) + { + var newFilter = filter.BuildNew(i == -1 ? "this" : $"filter{i}"); + sb.AppendLine($"var filter{++i} = {newFilter};"); + } + sb.AppendLine($"return filter{i};"); + } + + using (sb.BeginBlock($"public override Task InvokeAsync(CancellationToken cancellationToken)")) + { + var cmdArgs = needsCommand ? ", command" : ""; + sb.AppendLine($"return RunCommand{command.Id}Async(args{cmdArgs}, cancellationToken);"); + } + } + } } internal record CommandWithId(string? FieldType, Command Command, int Id); diff --git a/src/ConsoleAppFramework5/Parser.cs b/src/ConsoleAppFramework5/Parser.cs index 3bca9cd..c3a2f54 100644 --- a/src/ConsoleAppFramework5/Parser.cs +++ b/src/ConsoleAppFramework5/Parser.cs @@ -4,7 +4,7 @@ namespace ConsoleAppFramework; -internal class Parser(SourceProductionContext context, InvocationExpressionSyntax node, SemanticModel model, WellKnownTypes wellKnownTypes, DelegateBuildType delegateBuildType) +internal class Parser(SourceProductionContext context, InvocationExpressionSyntax node, SemanticModel model, WellKnownTypes wellKnownTypes, DelegateBuildType delegateBuildType, FilterInfo[] globalFilters) { public Command? ParseAndValidate() // for ConsoleApp.Run { @@ -23,7 +23,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta return null; } - public Command? ParseAndValidateForCommand() // for ConsoleAppBuilder.Add + public Command? ParseAndValidateForBuilderDelegateRegistration() // for ConsoleAppBuilder.Add { // Add(string commandName) var args = node.ArgumentList.Arguments; @@ -57,7 +57,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta return null; } - public Command?[] ParseForBuilderClassRegistration() + public Command?[] ParseAndValidateForBuilderClassRegistration() { // Add var genericName = (node.Expression as MemberAccessExpressionSyntax)?.Name as GenericNameSyntax; @@ -351,7 +351,8 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta Parameters = parameters, MethodKind = MethodKind.Lambda, Description = "", - DelegateBuildType = delegateBuildType + DelegateBuildType = delegateBuildType, + Filters = globalFilters, }; return cmd; @@ -480,7 +481,8 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta Parameters = parameters, MethodKind = addressOf ? MethodKind.FunctionPointer : MethodKind.Method, Description = summary, - DelegateBuildType = delegateBuildType + DelegateBuildType = delegateBuildType, + Filters = globalFilters, // TODO: combine class filter and method filter }; return cmd; diff --git a/src/ConsoleAppFramework5/SourceBuilder.cs b/src/ConsoleAppFramework5/SourceBuilder.cs index b1a8d90..d196585 100644 --- a/src/ConsoleAppFramework5/SourceBuilder.cs +++ b/src/ConsoleAppFramework5/SourceBuilder.cs @@ -47,6 +47,8 @@ public Block BeginBlock(string code) return new Block(this); } + public IDisposable Nop => NullDisposable.Instance; + public void AppendLine() { builder.AppendLine(); @@ -87,4 +89,13 @@ public void Dispose() parent.AppendLine("}"); } } + + class NullDisposable : IDisposable + { + public static readonly IDisposable Instance = new NullDisposable(); + + public void Dispose() + { + } + } } \ No newline at end of file From 18af412755549bd460c883bfb3dbaa48c2172196 Mon Sep 17 00:00:00 2001 From: neuecc Date: Tue, 28 May 2024 21:31:06 +0900 Subject: [PATCH 32/54] class filter/method filter --- sandbox/GeneratorSandbox/Program.cs | 28 +++++++--- src/ConsoleAppFramework5/Command.cs | 26 ++++++++++ .../ConsoleAppGenerator.cs | 17 ++---- src/ConsoleAppFramework5/Emitter.cs | 4 +- src/ConsoleAppFramework5/Parser.cs | 52 +++++++++++++++++-- 5 files changed, 101 insertions(+), 26 deletions(-) diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index abe3ce5..ca623ce 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -21,8 +21,9 @@ // args = ["do"]; // test. +args = ["sum", "--x", "10", "--y", "20"]; -var builder = ConsoleApp.CreateBuilder(); +//var builder = ConsoleApp.CreateBuilder(); //builder.Add("", () => { }); //builder.Add("a", () => { }); @@ -36,17 +37,21 @@ //builder.Run(args); -builder.AddFilter(); -builder.AddFilter(); +//builder.AddFilter(); +//builder.AddFilter(); -builder.Add("", (CancellationToken ct) => { Console.WriteLine("body"); }); +// builder.Add("", (int x, CancellationToken ct, int y) => { Console.WriteLine("body"); }); -builder.Run(args); +//builder.Add(); + +//builder.Run(args); +var mc = new MyClass(); +// ConsoleApp.Run(args, mc.Sum); @@ -125,6 +130,7 @@ static void Tests() } +[ConsoleAppFilter] public class MyClass { public void Do(CancellationToken cancellationToken) @@ -132,9 +138,10 @@ public void Do(CancellationToken cancellationToken) Console.Write("yeah"); } + [ConsoleAppFilter] public void Sum(int x, int y) { - Console.Write(x + y); + Console.WriteLine(x + y); } public void Echo(string msg) @@ -546,6 +553,15 @@ public override async Task InvokeAsync(CancellationToken cancellationToken) } } + public class NanimosinaiFilter(ConsoleAppFilter next) + : ConsoleAppFilter(next) + { + public override Task InvokeAsync(CancellationToken cancellationToken) + { + Console.WriteLine("filter0"); + return Next.InvokeAsync(cancellationToken); + } + } public class MyContext : IServiceProvider diff --git a/src/ConsoleAppFramework5/Command.cs b/src/ConsoleAppFramework5/Command.cs index c11a564..5f963ea 100644 --- a/src/ConsoleAppFramework5/Command.cs +++ b/src/ConsoleAppFramework5/Command.cs @@ -329,6 +329,32 @@ public record class FilterInfo public required string TypeFullName { get; init; } public required ITypeSymbol[] ConstructorParameterTypes { get; init; } + FilterInfo() + { + + } + + public static FilterInfo? Create(ITypeSymbol type) + { + var publicConstructors = type.GetMembers() + .OfType() + .Where(x => x.MethodKind == Microsoft.CodeAnalysis.MethodKind.Constructor && x.DeclaredAccessibility == Accessibility.Public) + .ToArray(); + + if (publicConstructors.Length != 1) + { + return null; + } + + var filter = new FilterInfo + { + TypeFullName = type.ToFullyQualifiedFormatDisplayString(), + ConstructorParameterTypes = publicConstructors[0].Parameters.Select(x => x.Type).ToArray() + }; + + return filter; + } + public string BuildNew(string nextFilterName) { var p = ConstructorParameterTypes.Select(parameter => diff --git a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs index b32ad8a..13fe8e1 100644 --- a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs @@ -532,23 +532,14 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex var type = model.GetTypeInfo(genericType).Type; if (type == null) return null!; - var publicConstructors = type.GetMembers() - .OfType() - .Where(x => x.MethodKind == Microsoft.CodeAnalysis.MethodKind.Constructor && x.DeclaredAccessibility == Accessibility.Public) - .ToArray(); + var filter = FilterInfo.Create(type); - if (publicConstructors.Length != 1) + if (filter == null) { - // TODO: validation + // TODO: validation, ctor is invalid. } - var filter = new FilterInfo - { - TypeFullName = type.ToFullyQualifiedFormatDisplayString(), - ConstructorParameterTypes = publicConstructors[0].Parameters.Select(x => x.Type).ToArray() - }; - - return filter; + return filter!; }) .Where(x => x != null) .ToArray(); diff --git a/src/ConsoleAppFramework5/Emitter.cs b/src/ConsoleAppFramework5/Emitter.cs index ed287de..9b3f92f 100644 --- a/src/ConsoleAppFramework5/Emitter.cs +++ b/src/ConsoleAppFramework5/Emitter.cs @@ -506,7 +506,8 @@ void EmitFilterInvoker(CommandWithId command) } sb.AppendLine($"return filter{i};"); } - + + sb.AppendLine(); using (sb.BeginBlock($"public override Task InvokeAsync(CancellationToken cancellationToken)")) { var cmdArgs = needsCommand ? ", command" : ""; @@ -518,4 +519,3 @@ void EmitFilterInvoker(CommandWithId command) internal record CommandWithId(string? FieldType, Command Command, int Id); } - diff --git a/src/ConsoleAppFramework5/Parser.cs b/src/ConsoleAppFramework5/Parser.cs index c3a2f54..3bc99b9 100644 --- a/src/ConsoleAppFramework5/Parser.cs +++ b/src/ConsoleAppFramework5/Parser.cs @@ -113,6 +113,27 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta var hasIDisposable = type.AllInterfaces.Any(x => SymbolEqualityComparer.Default.Equals(x, wellKnownTypes.IDisposable)); var hasIAsyncDisposable = type.AllInterfaces.Any(x => SymbolEqualityComparer.Default.Equals(x, wellKnownTypes.IAsyncDisposable)); + var typeFilters = type.GetAttributes() + .Select(x => + { + if (x.AttributeClass?.Name == "ConsoleAppFilterAttribute") + { + var filterType = x.AttributeClass.TypeArguments[0]; + var filter = FilterInfo.Create(filterType); + + if (filter == null) + { + // TODO: validation, ctor is invalid. + return null!; + } + + return filter; + } + return null!; + }) + .Where(x => x != null) + .ToArray(); + var methodInfoBase = new CommandMethodInfo { TypeFullName = type.ToFullyQualifiedFormatDisplayString(), @@ -137,7 +158,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta commandName = x.Name.ToLowerInvariant(); } - var command = ParseFromMethodSymbol(x, false, commandPath, commandName); + var command = ParseFromMethodSymbol(x, false, commandPath, commandName, typeFilters); if (command == null) return null; command.CommandMethodInfo = methodInfoBase with { MethodName = x.Name }; @@ -158,7 +179,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta var methodSymbols = model.GetMemberGroup(operand); if (methodSymbols.Length > 0 && methodSymbols[0] is IMethodSymbol methodSymbol) { - return ParseFromMethodSymbol(methodSymbol, addressOf: true, commandPath, commandName); + return ParseFromMethodSymbol(methodSymbol, addressOf: true, commandPath, commandName, []); } } else @@ -166,7 +187,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta var methodSymbols = model.GetMemberGroup(expression); if (methodSymbols.Length > 0 && methodSymbols[0] is IMethodSymbol methodSymbol) { - return ParseFromMethodSymbol(methodSymbol, addressOf: false, commandPath, commandName); + return ParseFromMethodSymbol(methodSymbol, addressOf: false, commandPath, commandName, []); } } } @@ -358,7 +379,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta return cmd; } - Command? ParseFromMethodSymbol(IMethodSymbol methodSymbol, bool addressOf, string[] commandPath, string commandName) + Command? ParseFromMethodSymbol(IMethodSymbol methodSymbol, bool addressOf, string[] commandPath, string commandName, FilterInfo[] typeFilters) { var docComment = methodSymbol.DeclaringSyntaxReferences[0].GetSyntax().GetDocumentationCommentTriviaSyntax(); var summary = ""; @@ -421,6 +442,27 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta return null; } + var methodFilters = methodSymbol.GetAttributes() + .Select(x => + { + if (x.AttributeClass?.Name == "ConsoleAppFilterAttribute") + { + var filterType = x.AttributeClass.TypeArguments[0]; + var filter = FilterInfo.Create(filterType); + + if (filter == null) + { + // TODO: validation, ctor is invalid. + return null!; + } + + return filter; + } + return null!; + }) + .Where(x => x != null) + .ToArray(); + var parsableIndex = 0; var parameters = methodSymbol.Parameters .Select(x => @@ -482,7 +524,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta MethodKind = addressOf ? MethodKind.FunctionPointer : MethodKind.Method, Description = summary, DelegateBuildType = delegateBuildType, - Filters = globalFilters, // TODO: combine class filter and method filter + Filters = globalFilters.Concat(typeFilters).Concat(methodFilters).ToArray(), }; return cmd; From 7b74ed6163f438e0a2c2fee3ae13d96acc26ff8b Mon Sep 17 00:00:00 2001 From: neuecc Date: Wed, 29 May 2024 12:22:53 +0900 Subject: [PATCH 33/54] filter done --- sandbox/GeneratorSandbox/Program.cs | 24 +- .../ConsoleAppGenerator.cs | 8 +- .../DiagnosticDescriptors.cs | 6 +- .../CSharpGeneratorRunner.cs | 1 + .../DiagnosticsTest.cs | 24 ++ .../FilterTest.cs | 209 ++++++++++++++++++ 6 files changed, 256 insertions(+), 16 deletions(-) create mode 100644 tests/ConsoleAppFramework.GeneratorTests/FilterTest.cs diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index ca623ce..b2fc1ce 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -23,9 +23,9 @@ // args = ["do"]; // test. args = ["sum", "--x", "10", "--y", "20"]; -//var builder = ConsoleApp.CreateBuilder(); +var builder = ConsoleApp.CreateBuilder(); -//builder.Add("", () => { }); +builder.Add("", () => { }); //builder.Add("a", () => { }); //builder.Add("a/b1", () => { }); //builder.Add("a/b2", () => { }); @@ -37,23 +37,21 @@ //builder.Run(args); -//builder.AddFilter(); -//builder.AddFilter(); +builder.AddFilter(); +builder.AddFilter(); // builder.Add("", (int x, CancellationToken ct, int y) => { Console.WriteLine("body"); }); -//builder.Add(); +builder.Add(); -//builder.Run(args); +builder.Run(args); var mc = new MyClass(); -// ConsoleApp.Run(args, mc.Sum); - - +//ConsoleApp.Run(args, Hello); @@ -65,6 +63,9 @@ +void Hello() +{ +} @@ -537,9 +538,6 @@ public class LogExecutionTimeFilter(ConsoleAppFilter next) { public override async Task InvokeAsync(CancellationToken cancellationToken) { - Console.WriteLine("filter2"); - - var startingTime = Stopwatch.GetTimestamp(); try { @@ -548,7 +546,7 @@ public override async Task InvokeAsync(CancellationToken cancellationToken) finally { var elapsed = Stopwatch.GetElapsedTime(startingTime); - ConsoleApp.Log($"Execution Time: {elapsed.ToString()}"); + ConsoleApp.Log($"Execution Time: {elapsed}"); } } } diff --git a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs index 13fe8e1..6cd3af5 100644 --- a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs @@ -117,7 +117,7 @@ public CommandAttribute(string command) internal abstract class ConsoleAppFilter(ConsoleAppFilter next) { - protected ConsoleAppFilter Next = next; + protected readonly ConsoleAppFilter Next = next; public abstract Task InvokeAsync(CancellationToken cancellationToken); } @@ -477,10 +477,14 @@ static void EmitConsoleAppRun(SourceProductionContext sourceProductionContext, ( { return; } + if (command.HasFilter) + { + sourceProductionContext.ReportDiagnostic(DiagnosticDescriptors.CommandHasFilter, node.GetLocation()); + return; + } var isRunAsync = ((node.Expression as MemberAccessExpressionSyntax)?.Name.Identifier.Text == "RunAsync"); - var sb = new SourceBuilder(0); sb.AppendLine(GeneratedCodeHeader); using (sb.BeginBlock("internal static partial class ConsoleApp")) diff --git a/src/ConsoleAppFramework5/DiagnosticDescriptors.cs b/src/ConsoleAppFramework5/DiagnosticDescriptors.cs index 548f2ec..774e9de 100644 --- a/src/ConsoleAppFramework5/DiagnosticDescriptors.cs +++ b/src/ConsoleAppFramework5/DiagnosticDescriptors.cs @@ -61,5 +61,9 @@ public static DiagnosticDescriptor Create(int id, string title, string messageFo public static DiagnosticDescriptor AddInLoopIsNotAllowed { get; } = Create( 8, - "ConsoleAppBuilder.Add/AddFilter is not allowed in loop statement(while, do, for, foreach)."); + "ConsoleAppBuilder.Add/AddFilter is not allowed in loop statements(while, do, for, foreach)."); + + public static DiagnosticDescriptor CommandHasFilter { get; } = Create( + 9, + "ConsoleApp.Run does not allow the use of filters, but the function has a filter attribute."); } diff --git a/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs b/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs index 25f4f0c..b523a11 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs @@ -16,6 +16,7 @@ public static void InitializeCompilation() { var globalUsings = """ global using System; +global using System.Threading; global using System.Threading.Tasks; global using System.ComponentModel.DataAnnotations; global using ConsoleAppFramework; diff --git a/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs b/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs index 5ca180f..d0ed2e6 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs @@ -244,4 +244,28 @@ public async Task Do() builder.Add("foo", async Task (int x, int y) => { return "foo"; }); """, "Task"); } + + + + [Fact] + public void RunAndFilter() + { + verifier.Verify(9, """ +ConsoleApp.Run(args, Hello); + +[ConsoleAppFilter] +void Hello() +{ +} + +public class NopFilter(ConsoleAppFilter next) + : ConsoleAppFilter(next) +{ + public override Task InvokeAsync(CancellationToken cancellationToken) + { + return Next.InvokeAsync(cancellationToken); + } +} +""", "ConsoleApp.Run(args, Hello)"); + } } diff --git a/tests/ConsoleAppFramework.GeneratorTests/FilterTest.cs b/tests/ConsoleAppFramework.GeneratorTests/FilterTest.cs new file mode 100644 index 0000000..83d45a2 --- /dev/null +++ b/tests/ConsoleAppFramework.GeneratorTests/FilterTest.cs @@ -0,0 +1,209 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit.Abstractions; + +namespace ConsoleAppFramework.GeneratorTests; + +public class FilterTest(ITestOutputHelper output) +{ + VerifyHelper verifier = new VerifyHelper(output, "CAF"); + + [Fact] + public void ForLambda() + { + verifier.Execute(""" +var builder = ConsoleApp.CreateBuilder(); + +builder.AddFilter(); +builder.AddFilter(); + +builder.Add("", Hello); + +builder.Run(args); + +[ConsoleAppFilter] +[ConsoleAppFilter] +void Hello() +{ + Console.Write("abcde"); +} + +internal class NopFilter1(ConsoleAppFilter next) + : ConsoleAppFilter(next) +{ + public override Task InvokeAsync(CancellationToken cancellationToken) + { + Console.Write(1); + return Next.InvokeAsync(cancellationToken); + } +} + +internal class NopFilter2(ConsoleAppFilter next) + : ConsoleAppFilter(next) +{ + public override Task InvokeAsync(CancellationToken cancellationToken) + { + Console.Write(2); + return Next.InvokeAsync(cancellationToken); + } +} + +internal class NopFilter3(ConsoleAppFilter next) + : ConsoleAppFilter(next) +{ + public override Task InvokeAsync(CancellationToken cancellationToken) + { + Console.Write(3); + return Next.InvokeAsync(cancellationToken); + } +} + +internal class NopFilter4(ConsoleAppFilter next) + : ConsoleAppFilter(next) +{ + public override Task InvokeAsync(CancellationToken cancellationToken) + { + Console.Write(4); + return Next.InvokeAsync(cancellationToken); + } +} +""", args: "", expected: "1234abcde"); + } + + [Fact] + public void ForClass() + { + verifier.Execute(""" +var builder = ConsoleApp.CreateBuilder(); + +builder.AddFilter(); +builder.AddFilter(); + +builder.Add(); + +await builder.RunAsync(args); + +[ConsoleAppFilter] +[ConsoleAppFilter] +public class MyClass +{ + [ConsoleAppFilter] + [ConsoleAppFilter] + public void Hello() + { + Console.Write("abcde"); + } +} + +internal class NopFilter1(ConsoleAppFilter next) + : ConsoleAppFilter(next) +{ + public override Task InvokeAsync(CancellationToken cancellationToken) + { + Console.Write(1); + return Next.InvokeAsync(cancellationToken); + } +} + +internal class NopFilter2(ConsoleAppFilter next) + : ConsoleAppFilter(next) +{ + public override Task InvokeAsync(CancellationToken cancellationToken) + { + Console.Write(2); + return Next.InvokeAsync(cancellationToken); + } +} + +internal class NopFilter3(ConsoleAppFilter next) + : ConsoleAppFilter(next) +{ + public override Task InvokeAsync(CancellationToken cancellationToken) + { + Console.Write(3); + return Next.InvokeAsync(cancellationToken); + } +} + +internal class NopFilter4(ConsoleAppFilter next) + : ConsoleAppFilter(next) +{ + public override Task InvokeAsync(CancellationToken cancellationToken) + { + Console.Write(4); + return Next.InvokeAsync(cancellationToken); + } +} + +internal class NopFilter5(ConsoleAppFilter next) + : ConsoleAppFilter(next) +{ + public override Task InvokeAsync(CancellationToken cancellationToken) + { + Console.Write(5); + return Next.InvokeAsync(cancellationToken); + } +} + +internal class NopFilter6(ConsoleAppFilter next) + : ConsoleAppFilter(next) +{ + public override Task InvokeAsync(CancellationToken cancellationToken) + { + Console.Write(6); + return Next.InvokeAsync(cancellationToken); + } +} +""", args: "hello", expected: "123456abcde"); + } + + + [Fact] + public void DI() + { + verifier.Execute(""" +var serviceCollection = new MiniDI(); +serviceCollection.Register(typeof(string), "hoge!"); +serviceCollection.Register(typeof(int), 9999); +ConsoleApp.ServiceProvider = serviceCollection; + +var builder = ConsoleApp.CreateBuilder(); + +builder.AddFilter(); + +builder.Add("", () => Console.Write("do")); + +builder.Run(args); + +internal class DIFilter(string foo, int bar, ConsoleAppFilter next) + : ConsoleAppFilter(next) +{ + public override Task InvokeAsync(CancellationToken cancellationToken) + { + Console.Write("invoke:"); + Console.Write(foo); + Console.Write(bar); + return Next.InvokeAsync(cancellationToken); + } +} + +public class MiniDI : IServiceProvider +{ + System.Collections.Generic.Dictionary dict = new(); + + public void Register(Type type, object instance) + { + dict[type] = instance; + } + + public object GetService(Type serviceType) + { + return dict.TryGetValue(serviceType, out var instance) ? instance : null; + } +} +""", args: "", expected: "invoke:hoge!9999do"); + } +} From 9c2e0536099c5672983407d44cc8cd24a07761bc Mon Sep 17 00:00:00 2001 From: neuecc Date: Wed, 29 May 2024 17:14:41 +0900 Subject: [PATCH 34/54] params support done --- sandbox/GeneratorSandbox/Program.cs | 92 ++++++++++++++++--- src/ConsoleAppFramework5/Command.cs | 14 ++- .../ConsoleAppGenerator.cs | 15 ++- src/ConsoleAppFramework5/Emitter.cs | 19 ++-- src/ConsoleAppFramework5/Parser.cs | 4 + .../ArrayPraseTest.cs | 68 ++++++++++++++ 6 files changed, 187 insertions(+), 25 deletions(-) create mode 100644 tests/ConsoleAppFramework.GeneratorTests/ArrayPraseTest.cs diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index b2fc1ce..700722c 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -21,31 +21,70 @@ // args = ["do"]; // test. -args = ["sum", "--x", "10", "--y", "20"]; +args = ["--ix", "[]", "--sx", "[]"]; -var builder = ConsoleApp.CreateBuilder(); +//var builder = ConsoleApp.CreateBuilder(); -builder.Add("", () => { }); -//builder.Add("a", () => { }); -//builder.Add("a/b1", () => { }); -//builder.Add("a/b2", () => { }); -//builder.Add("a/b2/c", () => { }); -//builder.Add("do", () => { }); +//builder.Add("", () => { }); +////builder.Add("a", () => { }); +////builder.Add("a/b1", () => { }); +////builder.Add("a/b2", () => { }); +////builder.Add("a/b2/c", () => { }); +////builder.Add("do", () => { }); + +////builder.Add("age"); + +////builder.Run(args); + + +//builder.AddFilter(); +//builder.AddFilter(); + + + +//// builder.Add("", (int x, CancellationToken ct, int y) => { Console.WriteLine("body"); }); + +//builder.Add(); -//builder.Add("age"); //builder.Run(args); +//stq//ring[] a = []; +//var i = 2; +//TryParseParamsArray(args, ref a, ref i); + + +ConsoleApp.Run(args, (int[] ix, string[] sx) => +{ + Console.Write("[" + string.Join(", ", ix) + "]"); + Console.Write("[" + string.Join(", ", sx) + "]"); +}); + + + +void Tako(ref int x) +{ +} + +static bool TryParseParamsArray(ReadOnlySpan args, ref string[] result, ref int i) +{ + result = new string[args.Length - i]; + var resultIndex = 0; + for (; i < args.Length; i++) + { + result[resultIndex++] = args[i]; + } + return true; +} + + + + -builder.AddFilter(); -builder.AddFilter(); -// builder.Add("", (int x, CancellationToken ct, int y) => { Console.WriteLine("body"); }); -builder.Add(); -builder.Run(args); @@ -63,7 +102,7 @@ -void Hello() +void Hello(int foo, params string[] aiueo) { } @@ -411,6 +450,29 @@ namespace ConsoleAppFramework { partial class ConsoleApp { + //static bool TryParseParamsArray(ReadOnlySpan args, ref string[] result, ref int i) + //{ + // result = new string[args.Length - i]; + // var resultIndex = 0; + // for (; i < args.Length; i++) + // { + // result[resultIndex++] = args[++i]; + // } + // return true; + //} + + //static bool TryParseParamsArray(ReadOnlySpan args, ref T[] result, ref int i) + // where T : ISpanParsable + //{ + // 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; + //} + partial struct ConsoleAppBuilder diff --git a/src/ConsoleAppFramework5/Command.cs b/src/ConsoleAppFramework5/Command.cs index 5f963ea..50f6433 100644 --- a/src/ConsoleAppFramework5/Command.cs +++ b/src/ConsoleAppFramework5/Command.cs @@ -39,7 +39,7 @@ public record class Command { if (DelegateBuildType == DelegateBuildType.MakeDelegateWhenHasDefaultValue) { - if (MethodKind == MethodKind.Lambda && Parameters.Any(x => x.HasDefaultValue)) + if (MethodKind == MethodKind.Lambda && Parameters.Any(x => x.HasDefaultValue || x.IsParams)) { delegateType = BuildDelegateType("RunCommand"); return "RunCommand"; @@ -150,6 +150,7 @@ public record class CommandParameter public required ITypeSymbol Type { get; init; } public required Location Location { get; init; } public required bool IsNullableReference { get; init; } + public required bool IsParams { get; init; } public required string Name { get; init; } public required bool HasDefaultValue { get; init; } public object? DefaultValue { get; init; } @@ -162,6 +163,7 @@ public record class CommandParameter public bool IsArgument => ArgumentIndex != -1; public required string[] Aliases { get; init; } public required string Description { get; init; } + public bool RequireCheckArgumentParsed => !(HasDefaultValue || IsParams); public string BuildParseMethod(int argCount, string argumentName, WellKnownTypes wellKnownTypes, bool increment) { @@ -216,6 +218,12 @@ string Core(ITypeSymbol type, bool nullable) return $"if (!Enum.TryParse<{type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>(args[{index}], true, {outArgVar})) {{ ThrowArgumentParseFailed(\"{argumentName}\", args[i]); }}{elseExpr}"; } + // ParamsArray + if (IsParams) + { + return $"{(increment ? "i++; " : "")}if (!TryParseParamsArray(args, ref arg{argCount}, ref i)) {{ ThrowArgumentParseFailed(\"{argumentName}\", args[i]); }}{elseExpr}"; + } + // Array if (type.TypeKind == TypeKind.Array) { @@ -291,6 +299,10 @@ public string ToTypeDisplayString() public override string ToString() { var sb = new StringBuilder(); + if (IsParams) + { + sb.Append("params "); + } sb.Append(ToTypeDisplayString()); sb.Append(" "); sb.Append(Name); diff --git a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs index 6cd3af5..c7bfd48 100644 --- a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs @@ -173,6 +173,18 @@ static void ThrowArgumentNameNotFound(string argumentName) throw new ArgumentException($"Argument '{argumentName}' does not found in command prameters."); } + 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 { @@ -181,6 +193,7 @@ static bool TrySplitParse(ReadOnlySpan s, out T[] result) try { result = System.Text.Json.JsonSerializer.Deserialize(s)!; + return true; } catch { @@ -493,7 +506,7 @@ static void EmitConsoleAppRun(SourceProductionContext sourceProductionContext, ( emitter.EmitRun(sb, command, isRunAsync); } - sourceProductionContext.AddSource("ConsoleApp.Builder.g.cs", sb.ToString()); + sourceProductionContext.AddSource("ConsoleApp.Run.g.cs", sb.ToString()); } static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContext, ImmutableArray<(InvocationExpressionSyntax Node, string Name, SemanticModel Model)> generatorSyntaxContexts) diff --git a/src/ConsoleAppFramework5/Emitter.cs b/src/ConsoleAppFramework5/Emitter.cs index 9b3f92f..06331ff 100644 --- a/src/ConsoleAppFramework5/Emitter.cs +++ b/src/ConsoleAppFramework5/Emitter.cs @@ -1,4 +1,5 @@ using Microsoft.CodeAnalysis; +using System.Reflection.Metadata; namespace ConsoleAppFramework; @@ -32,7 +33,7 @@ public void EmitRun(SourceBuilder sb, Command command, bool isRunAsync, string? // emit custom delegate type if (delegateType != null && !emitForBuilder) { - sb.AppendLine($"internal {{delgateType}}"); + sb.AppendLine($"internal {delegateType}"); sb.AppendLine(); } @@ -54,9 +55,11 @@ public void EmitRun(SourceBuilder sb, Command command, bool isRunAsync, string? var parameter = command.Parameters[i]; if (parameter.IsParsable) { - var defaultValue = parameter.HasDefaultValue ? parameter.DefaultValueToString() : $"default({parameter.Type.ToFullyQualifiedFormatDisplayString()})"; + var defaultValue = parameter.IsParams ? $"({parameter.ToTypeDisplayString()})[]" + : parameter.HasDefaultValue ? parameter.DefaultValueToString() + : $"default({parameter.Type.ToFullyQualifiedFormatDisplayString()})"; sb.AppendLine($"var arg{i} = {defaultValue};"); - if (!parameter.HasDefaultValue) + if (parameter.RequireCheckArgumentParsed) { sb.AppendLine($"var arg{i}Parsed = false;"); } @@ -96,7 +99,7 @@ public void EmitRun(SourceBuilder sb, Command command, bool isRunAsync, string? using (sb.BeginBlock()) { sb.AppendLine($"{parameter.BuildParseMethod(i, parameter.Name, wellKnownTypes, increment: false)}"); - if (!parameter.HasDefaultValue) + if (parameter.RequireCheckArgumentParsed) { sb.AppendLine($"arg{i}Parsed = true;"); } @@ -126,7 +129,7 @@ public void EmitRun(SourceBuilder sb, Command command, bool isRunAsync, string? using (sb.BeginBlock()) { sb.AppendLine($"{parameter.BuildParseMethod(i, parameter.Name, wellKnownTypes, increment: true)}"); - if (!parameter.HasDefaultValue) + if (parameter.RequireCheckArgumentParsed) { sb.AppendLine($"arg{i}Parsed = true;"); } @@ -152,7 +155,7 @@ public void EmitRun(SourceBuilder sb, Command command, bool isRunAsync, string? using (sb.BeginBlock()) { sb.AppendLine($"{parameter.BuildParseMethod(i, parameter.Name, wellKnownTypes, increment: true)}"); - if (!parameter.HasDefaultValue) + if (parameter.RequireCheckArgumentParsed) { sb.AppendLine($"arg{i}Parsed = true;"); } @@ -172,7 +175,7 @@ public void EmitRun(SourceBuilder sb, Command command, bool isRunAsync, string? var parameter = command.Parameters[i]; if (!parameter.IsParsable) continue; - if (!parameter.HasDefaultValue) + if (parameter.RequireCheckArgumentParsed) { sb.AppendLine($"if (!arg{i}Parsed) ThrowRequiredArgumentNotParsed(\"{parameter.Name}\");"); } @@ -506,7 +509,7 @@ void EmitFilterInvoker(CommandWithId command) } sb.AppendLine($"return filter{i};"); } - + sb.AppendLine(); using (sb.BeginBlock($"public override Task InvokeAsync(CancellationToken cancellationToken)")) { diff --git a/src/ConsoleAppFramework5/Parser.cs b/src/ConsoleAppFramework5/Parser.cs index 3bc99b9..bdee716 100644 --- a/src/ConsoleAppFramework5/Parser.cs +++ b/src/ConsoleAppFramework5/Parser.cs @@ -277,6 +277,8 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta defaultValue = false; } + var hasParams = x.Modifiers.Any(x => x.IsKind(SyntaxKind.ParamsKeyword)); + var customParserType = x.AttributeLists.SelectMany(x => x.Attributes) .Select(x => { @@ -347,6 +349,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta { Name = x.Identifier.Text, IsNullableReference = isNullableReference, + IsParams = hasParams, Type = type.Type!, Location = x.GetLocation(), HasDefaultValue = hasDefault, @@ -499,6 +502,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta { Name = x.Name, IsNullableReference = isNullableReference, + IsParams = x.IsParams, Location = x.DeclaringSyntaxReferences[0].GetSyntax().GetLocation(), Type = x.Type, HasDefaultValue = x.HasExplicitDefaultValue, diff --git a/tests/ConsoleAppFramework.GeneratorTests/ArrayPraseTest.cs b/tests/ConsoleAppFramework.GeneratorTests/ArrayPraseTest.cs new file mode 100644 index 0000000..b5d932e --- /dev/null +++ b/tests/ConsoleAppFramework.GeneratorTests/ArrayPraseTest.cs @@ -0,0 +1,68 @@ +using Xunit.Abstractions; + +namespace ConsoleAppFramework.GeneratorTests +{ + public class ArrayPraseTest(ITestOutputHelper output) + { + VerifyHelper verifier = new VerifyHelper(output, "CAF"); + + [Fact] + public void Params() + { + var code = """ +ConsoleApp.Run(args, (params int[] foo) => +{ + Console.Write("[" + string.Join(", ", foo) + "]"); +}); +"""; + verifier.Execute(code, args: "--foo", expected: "[]"); + verifier.Execute(code, args: "--foo 10", expected: "[10]"); + verifier.Execute(code, args: "--foo 10 20 30", expected: "[10, 20, 30]"); + } + + [Fact] + public void ArgumentParams() + { + var code = """ +ConsoleApp.Run(args, ([Argument]string title, [Argument]params int[] foo) => +{ + Console.Write(title + "[" + string.Join(", ", foo) + "]"); +}); +"""; + verifier.Execute(code, args: "aiueo", expected: "aiueo[]"); + verifier.Execute(code, args: "aiueo 10", expected: "aiueo[10]"); + verifier.Execute(code, args: "aiueo 10 20 30", expected: "aiueo[10, 20, 30]"); + } + + [Fact] + public void ParseArray() + { + var code = """ +ConsoleApp.Run(args, (int[] ix, string[] sx) => +{ + Console.Write("[" + string.Join(", ", ix) + "]"); + Console.Write("[" + string.Join(", ", sx) + "]"); +}); +"""; + verifier.Execute(code, args: "--ix 1,2,3,4,5 --sx a,b,c,d,e", expected: "[1, 2, 3, 4, 5][a, b, c, d, e]"); + + var largeIntArray = string.Join(",", Enumerable.Range(0, 1000)); + var expectedIntArray = string.Join(", ", Enumerable.Range(0, 1000)); + verifier.Execute(code, args: $"--ix {largeIntArray} --sx a,b,c,d,e", expected: $"[{expectedIntArray}][a, b, c, d, e]"); + } + + [Fact] + public void JsonArray() + { + var code = """ +ConsoleApp.Run(args, (int[] ix, string[] sx) => +{ + Console.Write("[" + string.Join(", ", ix) + "]"); + Console.Write("[" + string.Join(", ", sx) + "]"); +}); +"""; + verifier.Execute(code, args: "--ix [] --sx []", expected: "[][]"); + verifier.Execute(code, args: "--ix [1,2,3,4,5] --sx [\"a\",\"b\",\"c\",\"d\",\"e\"]", expected: "[1, 2, 3, 4, 5][a, b, c, d, e]"); + } + } +} From a6d342a31d6c5de19528392d608821a5c82741a3 Mon Sep 17 00:00:00 2001 From: neuecc Date: Wed, 29 May 2024 20:24:36 +0900 Subject: [PATCH 35/54] help1 --- sandbox/GeneratorSandbox/Program.cs | 95 +----- .../CommandHelpBuilder.cs | 322 ++++++++++++++++++ .../ConsoleAppGenerator.cs | 24 +- src/ConsoleAppFramework5/Emitter.cs | 26 +- src/ConsoleAppFramework5/NameConverter.cs | 31 ++ src/ConsoleAppFramework5/Parser.cs | 8 +- src/ConsoleAppFramework5/SourceBuilder.cs | 5 + .../NameConverterTest.cs | 83 +++++ 8 files changed, 486 insertions(+), 108 deletions(-) create mode 100644 src/ConsoleAppFramework5/CommandHelpBuilder.cs create mode 100644 src/ConsoleAppFramework5/NameConverter.cs create mode 100644 tests/ConsoleAppFramework.GeneratorTests/NameConverterTest.cs diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index 700722c..65ba270 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -21,89 +21,18 @@ // args = ["do"]; // test. -args = ["--ix", "[]", "--sx", "[]"]; +//args = ["--foo-bar", "aiueo"]; -//var builder = ConsoleApp.CreateBuilder(); +ConsoleApp.Run(args, (int x, bool tako, int y = 999) => { }); -//builder.Add("", () => { }); -////builder.Add("a", () => { }); -////builder.Add("a/b1", () => { }); -////builder.Add("a/b2", () => { }); -////builder.Add("a/b2/c", () => { }); -////builder.Add("do", () => { }); - -////builder.Add("age"); - -////builder.Run(args); - - -//builder.AddFilter(); -//builder.AddFilter(); - - - -//// builder.Add("", (int x, CancellationToken ct, int y) => { Console.WriteLine("body"); }); - -//builder.Add(); - - -//builder.Run(args); - -//stq//ring[] a = []; -//var i = 2; -//TryParseParamsArray(args, ref a, ref i); - - -ConsoleApp.Run(args, (int[] ix, string[] sx) => +public class MyClass333 { - Console.Write("[" + string.Join(", ", ix) + "]"); - Console.Write("[" + string.Join(", ", sx) + "]"); -}); - - - -void Tako(ref int x) -{ -} - -static bool TryParseParamsArray(ReadOnlySpan args, ref string[] result, ref int i) -{ - result = new string[args.Length - i]; - var resultIndex = 0; - for (; i < args.Length; i++) + public void HelloWorld(string fooBar) { - result[resultIndex++] = args[i]; + Console.Write("Hello World! " + fooBar); } - return true; -} - - - - - - - - - - -var mc = new MyClass(); - -//ConsoleApp.Run(args, Hello); - - - - - - - - - - - -void Hello(int foo, params string[] aiueo) -{ } @@ -128,8 +57,6 @@ void Hello(int foo, params string[] aiueo) - - @@ -154,21 +81,11 @@ void Hello(int foo, params string[] aiueo) // --x -static async Task RunRun(int? x = null, string? y = null) -{ - await Task.Yield(); - Console.WriteLine("Hello World!" + x + y); - return 0; -} -static void Tests() - where T : ISpanParsable -{ -} [ConsoleAppFilter] public class MyClass @@ -507,7 +424,7 @@ partial class ConsoleApp { private static async Task RunAsyncCommand1(string[] args) { - if (TryShowHelpOrVersion(args, 0)) return; + // if (TryShowHelpOrVersion(args, 0)) return; using var posixSignalHandler = PosixSignalHandler.Register(Timeout); var arg0 = posixSignalHandler.Token; diff --git a/src/ConsoleAppFramework5/CommandHelpBuilder.cs b/src/ConsoleAppFramework5/CommandHelpBuilder.cs new file mode 100644 index 0000000..43933ab --- /dev/null +++ b/src/ConsoleAppFramework5/CommandHelpBuilder.cs @@ -0,0 +1,322 @@ +using Microsoft.CodeAnalysis; +using System.Text; + +namespace ConsoleAppFramework; + +public class CommandHelpBuilder +{ + //public string BuildHelpMessage(CommandDescriptor? defaultCommand, IEnumerable commands, bool shortCommandName) + //{ + // var sb = new StringBuilder(); + + // bool showHeader = (defaultCommand != null); + // if (defaultCommand != null) + // { + // // Display a help messages for default method + // sb.Append(BuildHelpMessage(CreateCommandHelpDefinition(defaultCommand, shortCommandName), showCommandName: false, fromMultiCommand: false)); + // } + + // var orderedCommands = options.HelpSortCommandsByFullName + // ? commands.OrderBy(x => x.GetCommandName(options)).ToArray() + // : commands.OrderBy(x => x.GetNamesFormatted(options)).ToArray(); + // if (orderedCommands.Length > 0) + // { + // if (defaultCommand == null) + // { + // sb.Append(BuildUsageMessage()); + // sb.AppendLine(); + // } + + // sb.Append(BuildMethodListMessage(orderedCommands, shortCommandName, out var maxWidth)); + // } + + // return sb.ToString(); + //} + + public string BuildHelpMessage(Command command) + { + return BuildHelpMessage(CreateCommandHelpDefinition(command, false), showCommandName: false, fromMultiCommand: false); + } + + internal string BuildHelpMessage(CommandHelpDefinition definition, bool showCommandName, bool fromMultiCommand) + { + var sb = new StringBuilder(); + + sb.AppendLine(BuildUsageMessage(definition, showCommandName, fromMultiCommand)); + sb.AppendLine(); + + if (!string.IsNullOrEmpty(definition.Description)) + { + sb.AppendLine(definition.Description); + sb.AppendLine(); + } + + if (definition.Options.Any()) + { + sb.Append(BuildArgumentsMessage(definition)); + sb.Append(BuildOptionsMessage(definition)); + } + else + { + sb.AppendLine("Options:"); + sb.AppendLine(" ()"); + sb.AppendLine(); + } + + return sb.ToString(); + } + + internal string BuildUsageMessage() + { + var sb = new StringBuilder(); + sb.AppendLine($"Usage: "); + + return sb.ToString(); + } + + internal string BuildUsageMessage(CommandHelpDefinition definition, bool showCommandName, bool fromMultiCommand) + { + var sb = new StringBuilder(); + sb.Append($"Usage:"); + + if (showCommandName) + { + sb.Append($" {definition.Command}"); + } + + foreach (var opt in definition.Options.Where(x => x.Index.HasValue)) + { + sb.Append($" <{(string.IsNullOrEmpty(opt.Description) ? opt.Options[0] : opt.Description)}>"); + } + + if (definition.Options.Any(x => !x.Index.HasValue)) + { + sb.Append(" [options...]"); + } + + return sb.ToString(); + } + + internal string BuildArgumentsMessage(CommandHelpDefinition definition) + { + var argumentsFormatted = definition.Options + .Where(x => x.Index.HasValue) + .Select(x => (Argument: $"[{x.Index}] {x.FormattedValueTypeName}", x.Description)) + .ToArray(); + + if (!argumentsFormatted.Any()) return string.Empty; + + var maxWidth = argumentsFormatted.Max(x => x.Argument.Length); + + var sb = new StringBuilder(); + + sb.AppendLine("Arguments:"); + foreach (var arg in argumentsFormatted) + { + var padding = maxWidth - arg.Argument.Length; + + sb.Append(" "); + sb.Append(arg.Argument); + for (var i = 0; i < padding; i++) + { + sb.Append(' '); + } + + sb.Append(" "); + sb.AppendLine(arg.Description); + } + + sb.AppendLine(); + + return sb.ToString(); + } + + internal string BuildOptionsMessage(CommandHelpDefinition definition) + { + var optionsFormatted = definition.Options + .Where(x => !x.Index.HasValue) + .Select(x => (Options: string.Join(", ", x.Options) + (x.IsFlag ? string.Empty : $" {x.FormattedValueTypeName}"), x.Description, x.IsRequired, x.IsFlag, x.DefaultValue)) + .ToArray(); + + if (!optionsFormatted.Any()) return string.Empty; + + var maxWidth = optionsFormatted.Max(x => x.Options.Length); + + var sb = new StringBuilder(); + + sb.AppendLine("Options:"); + foreach (var opt in optionsFormatted) + { + var options = opt.Options; + var padding = maxWidth - options.Length; + + sb.Append(" "); + sb.Append(options); + for (var i = 0; i < padding; i++) + { + sb.Append(' '); + } + + sb.Append(" "); + sb.Append(opt.Description); + + if (opt.IsFlag) + { + sb.Append($" (Optional)"); + } + else if (opt.DefaultValue != null) + { + sb.Append($" (Default: {opt.DefaultValue})"); + } + else if (opt.IsRequired) + { + sb.Append($" (Required)"); + } + + sb.AppendLine(); + } + + sb.AppendLine(); + + return sb.ToString(); + } + + //internal string BuildMethodListMessage(IEnumerable types, bool shortCommandName, out int maxWidth) + //{ + // maxWidth = 0; + // return BuildMethodListMessage(types.Select(x => CreateCommandHelpDefinition(x, shortCommandName)), true, out maxWidth); + //} + + //internal string BuildMethodListMessage(IEnumerable commandHelpDefinitions, bool appendCommand, out int maxWidth) + //{ + // var formatted = commandHelpDefinitions + // .Select(x => (Command: $"{(x.CommandAliases.Length != 0 ? ((appendCommand ? x.Command + " " : "") + string.Join(", ", x.CommandAliases)) : x.Command)}", Description: x.Description)) + // .ToArray(); + // maxWidth = formatted.Max(x => x.Command.Length); + + // var sb = new StringBuilder(); + + // sb.AppendLine("Commands:"); + // foreach (var item in formatted) + // { + // sb.Append(" "); + // sb.Append(item.Command); + + // var padding = maxWidth - item.Command.Length; + // for (var i = 0; i < padding; i++) + // { + // sb.Append(' '); + // } + + // sb.Append(" "); + // sb.AppendLine(item.Description); + // } + + // return sb.ToString(); + //} + + internal CommandHelpDefinition CreateCommandHelpDefinition(Command descriptor, bool shortCommandName) + { + var parameterDefinitions = new List(); + + foreach (var item in descriptor.Parameters) + { + // ignore DI params. + if (!item.IsParsable) continue; + + // -i, -input | [default=foo]... + + var index = item.ArgumentIndex == -1 ? null : (int?)item.ArgumentIndex; + var options = new List(); + if (item.ArgumentIndex != -1) + { + options.Add($"[{item.ArgumentIndex}]"); + } + else + { + options.Add("--" + item.Name); + foreach (var alias in item.Aliases) + { + options.Add(alias); + } + } + + var description = item.Description; + var isFlag = item.Type.SpecialType == Microsoft.CodeAnalysis.SpecialType.System_Boolean; + + var defaultValue = default(string); + if (item.HasDefaultValue) + { + defaultValue = (item.DefaultValue?.ToString() ?? "null"); + if (isFlag) + { + if (item.DefaultValue is true) + { + // bool option with true default value is not flag. + isFlag = false; + } + else if (item.DefaultValue is false) + { + // false default value should be omitted for flag. + defaultValue = null; + } + } + } + + var paramTypeName = item.ToTypeDisplayString(); + if (item.Type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) + { + paramTypeName = ((item.Type as INamedTypeSymbol)!.TypeArguments[0]).ToFullyQualifiedFormatDisplayString() + "?"; + } + + parameterDefinitions.Add(new CommandOptionHelpDefinition(options.Distinct().ToArray(), description, paramTypeName, defaultValue, index, isFlag)); + } + + return new CommandHelpDefinition( + descriptor.CommandName, + // descriptor.Aliases, + parameterDefinitions.ToArray(), + descriptor.Description + ); + } + + public class CommandHelpDefinition + { + // TODO: Command Path + + public string Command { get; } + public CommandOptionHelpDefinition[] Options { get; } + public string Description { get; } + + public CommandHelpDefinition(string command, CommandOptionHelpDefinition[] options, string description) + { + Command = command; + Options = options; + Description = description; + } + } + + // TODO: params? + public class CommandOptionHelpDefinition + { + public string[] Options { get; } + public string Description { get; } + public string? DefaultValue { get; } + public string ValueTypeName { get; } + public int? Index { get; } + + public bool IsRequired => DefaultValue == null; + public bool IsFlag { get; } + public string FormattedValueTypeName => "<" + ValueTypeName + ">"; + + public CommandOptionHelpDefinition(string[] options, string description, string valueTypeName, string? defaultValue, int? index, bool isFlag) + { + Options = options; + Description = description; + ValueTypeName = valueTypeName; + DefaultValue = defaultValue; + Index = index; + IsFlag = isFlag; + } + } +} diff --git a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs index c7bfd48..7659773 100644 --- a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs @@ -261,13 +261,13 @@ static void ValidateParameter(object? value, ParameterInfo parameter, Validation } [MethodImpl(MethodImplOptions.AggressiveInlining)] - static bool TryShowHelpOrVersion(ReadOnlySpan args, int parameterCount) + static bool TryShowHelpOrVersion(ReadOnlySpan args, int parameterCount, int helpId) { if (args.Length == 0) { if (parameterCount == 0) return false; - ShowHelp(); + ShowHelp(helpId); return true; } @@ -280,7 +280,7 @@ static bool TryShowHelpOrVersion(ReadOnlySpan args, int parameterCount) return true; case "-h": case "--help": - ShowHelp(); + ShowHelp(helpId); return true; default: break; @@ -310,10 +310,7 @@ static void ShowVersion() Log(version); } - static void ShowHelp() - { - Log("TODO: Build Help"); - } + static partial void ShowHelp(int helpId); static async Task RunWithFilterAsync(ConsoleAppFilter invoker) { @@ -503,10 +500,19 @@ static void EmitConsoleAppRun(SourceProductionContext sourceProductionContext, ( using (sb.BeginBlock("internal static partial class ConsoleApp")) { var emitter = new Emitter(wellKnownTypes); - emitter.EmitRun(sb, command, isRunAsync); + var withId = new Emitter.CommandWithId(null, command, -1); + emitter.EmitRun(sb, withId, isRunAsync); } - sourceProductionContext.AddSource("ConsoleApp.Run.g.cs", sb.ToString()); + + var help = new SourceBuilder(0); + help.AppendLine(GeneratedCodeHeader); + using (help.BeginBlock("internal static partial class ConsoleApp")) + { + var emitter = new Emitter(wellKnownTypes); + emitter.EmitHelp(help, command); + } + sourceProductionContext.AddSource("ConsoleApp.Help.g.cs", help.ToString()); } static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContext, ImmutableArray<(InvocationExpressionSyntax Node, string Name, SemanticModel Model)> generatorSyntaxContexts) diff --git a/src/ConsoleAppFramework5/Emitter.cs b/src/ConsoleAppFramework5/Emitter.cs index 06331ff..fc0d571 100644 --- a/src/ConsoleAppFramework5/Emitter.cs +++ b/src/ConsoleAppFramework5/Emitter.cs @@ -5,8 +5,10 @@ namespace ConsoleAppFramework; internal class Emitter(WellKnownTypes wellKnownTypes) { - public void EmitRun(SourceBuilder sb, Command command, bool isRunAsync, string? methodName = null) + public void EmitRun(SourceBuilder sb, CommandWithId commandWithId, bool isRunAsync, string? methodName = null) { + var command = commandWithId.Command; + var emitForBuilder = methodName != null; var hasCancellationToken = command.Parameters.Any(x => x.IsCancellationToken); var hasArgument = command.Parameters.Any(x => x.IsArgument); @@ -42,7 +44,7 @@ public void EmitRun(SourceBuilder sb, Command command, bool isRunAsync, string? // method signature using (sb.BeginBlock($"{accessibility} static {unsafeCode}{returnType} {methodName}({argsType} args{commandMethodType}{filterCancellationToken})")) { - sb.AppendLine($"if (TryShowHelpOrVersion(args, {parsableParameterCount})) return;"); + sb.AppendLine($"if (TryShowHelpOrVersion(args, {parsableParameterCount}, {commandWithId.Id})) return;"); sb.AppendLine(); // prepare argument variables @@ -362,11 +364,11 @@ public void EmitBuilder(SourceBuilder sb, Command[] commands, bool emitSync, boo if (item.Command.HasFilter) { - EmitRun(sb, item.Command, true, $"RunCommand{item.Id}Async"); + EmitRun(sb, item, true, $"RunCommand{item.Id}Async"); } else { - EmitRun(sb, item.Command, false, $"RunCommand{item.Id}"); + EmitRun(sb, item, false, $"RunCommand{item.Id}"); } } } @@ -378,7 +380,7 @@ public void EmitBuilder(SourceBuilder sb, Command[] commands, bool emitSync, boo foreach (var item in commandIds) { if (!emittedCommand.Add(item.Command)) continue; - EmitRun(sb, item.Command, true, $"RunCommand{item.Id}Async"); + EmitRun(sb, item, true, $"RunCommand{item.Id}Async"); } } @@ -454,7 +456,7 @@ void EmitLeafCommand(CommandWithId? command) { if (command == null) { - sb.AppendLine("TryShowHelpOrVersion(args, -1);"); + sb.AppendLine("TryShowHelpOrVersion(args, -1, -1);"); } else { @@ -520,5 +522,17 @@ void EmitFilterInvoker(CommandWithId command) } } + public void EmitHelp(SourceBuilder sb, Command command) + { + var helpBuilder = new CommandHelpBuilder(); + var help = helpBuilder.BuildHelpMessage(command); + using (sb.BeginBlock("static partial void ShowHelp(int helpId)")) + { + sb.AppendLine("Log(\"\"\""); + sb.AppendLineWithoutIndent(help); + sb.AppendLineWithoutIndent("\"\"\");"); + } + } + internal record CommandWithId(string? FieldType, Command Command, int Id); } diff --git a/src/ConsoleAppFramework5/NameConverter.cs b/src/ConsoleAppFramework5/NameConverter.cs new file mode 100644 index 0000000..c5702c0 --- /dev/null +++ b/src/ConsoleAppFramework5/NameConverter.cs @@ -0,0 +1,31 @@ +using System.Text; + +namespace ConsoleAppFramework; + +public static class NameConverter +{ + public static string ToKebabCase(string name) + { + var sb = new StringBuilder(); + for (int i = 0; i < name.Length; i++) + { + if (!Char.IsUpper(name[i])) + { + sb.Append(name[i]); + continue; + } + + // Abc, abC, AB-c => first or Last or capital continuous, no added. + if (i == 0 || i == name.Length - 1 || Char.IsUpper(name[i + 1])) + { + sb.Append(Char.ToLowerInvariant(name[i])); + continue; + } + + // others, add- + sb.Append('-'); + sb.Append(Char.ToLowerInvariant(name[i])); + } + return sb.ToString(); + } +} diff --git a/src/ConsoleAppFramework5/Parser.cs b/src/ConsoleAppFramework5/Parser.cs index bdee716..4f45112 100644 --- a/src/ConsoleAppFramework5/Parser.cs +++ b/src/ConsoleAppFramework5/Parser.cs @@ -1,6 +1,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Xml.Linq; namespace ConsoleAppFramework; @@ -154,8 +155,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta } else { - // TODO: commandName convert to snake-case - commandName = x.Name.ToLowerInvariant(); + commandName = NameConverter.ToKebabCase(x.Name); } var command = ParseFromMethodSymbol(x, false, commandPath, commandName, typeFilters); @@ -347,7 +347,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta return new CommandParameter { - Name = x.Identifier.Text, + Name = NameConverter.ToKebabCase(x.Identifier.Text), IsNullableReference = isNullableReference, IsParams = hasParams, Type = type.Type!, @@ -500,7 +500,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta return new CommandParameter { - Name = x.Name, + Name = NameConverter.ToKebabCase(x.Name), IsNullableReference = isNullableReference, IsParams = x.IsParams, Location = x.DeclaringSyntaxReferences[0].GetSyntax().GetLocation(), diff --git a/src/ConsoleAppFramework5/SourceBuilder.cs b/src/ConsoleAppFramework5/SourceBuilder.cs index d196585..0c1f297 100644 --- a/src/ConsoleAppFramework5/SourceBuilder.cs +++ b/src/ConsoleAppFramework5/SourceBuilder.cs @@ -71,6 +71,11 @@ public void AppendLine(string text) builder.AppendLine(text); } + public void AppendLineWithoutIndent(string text) + { + builder.AppendLine(text); + } + public override string ToString() => builder.ToString(); public struct Scope(SourceBuilder parent) : IDisposable diff --git a/tests/ConsoleAppFramework.GeneratorTests/NameConverterTest.cs b/tests/ConsoleAppFramework.GeneratorTests/NameConverterTest.cs new file mode 100644 index 0000000..1702a66 --- /dev/null +++ b/tests/ConsoleAppFramework.GeneratorTests/NameConverterTest.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit.Abstractions; + +namespace ConsoleAppFramework.GeneratorTests; + +public class NameConverterTest(ITestOutputHelper output) +{ + VerifyHelper verifier = new VerifyHelper(output, "CAF"); + + [Fact] + public void KebabCase() + { + NameConverter.ToKebabCase("").Should().Be(""); + NameConverter.ToKebabCase("HelloWorld").Should().Be("hello-world"); + NameConverter.ToKebabCase("HelloWorldMyHome").Should().Be("hello-world-my-home"); + NameConverter.ToKebabCase("helloWorld").Should().Be("hello-world"); + NameConverter.ToKebabCase("hello-world").Should().Be("hello-world"); + NameConverter.ToKebabCase("A").Should().Be("a"); + NameConverter.ToKebabCase("AB").Should().Be("ab"); + NameConverter.ToKebabCase("ABC").Should().Be("abc"); + NameConverter.ToKebabCase("ABCD").Should().Be("abcd"); + NameConverter.ToKebabCase("ABCDeF").Should().Be("abc-def"); + NameConverter.ToKebabCase("XmlReader").Should().Be("xml-reader"); + NameConverter.ToKebabCase("XMLReader").Should().Be("xml-reader"); + NameConverter.ToKebabCase("MLLibrary").Should().Be("ml-library"); + } + + [Fact] + public void CommmandName() + { + verifier.Execute(""" +var builder = ConsoleApp.CreateBuilder(); +builder.Add(); +builder.Run(args); + +public class MyClass +{ + public void HelloWorld() + { + Console.Write("Hello World!"); + } +} +""", args: "hello-world", expected: "Hello World!"); + } + + [Fact] + public void OptionName() + { + verifier.Execute(""" +var builder = ConsoleApp.CreateBuilder(); +builder.Add(); +builder.Run(args); + +public class MyClass +{ + public void HelloWorld(string fooBar) + { + Console.Write("Hello World! " + fooBar); + } +} +""", args: "hello-world --foo-bar aiueo", expected: "Hello World! aiueo"); + + + verifier.Execute(""" +var builder = ConsoleApp.CreateBuilder(); +var mc = new MyClass(); +builder.Add("hello-world", mc.HelloWorld); +builder.Run(args); + +public class MyClass +{ + public void HelloWorld(string fooBar) + { + Console.Write("Hello World! " + fooBar); + } +} +""", args: "hello-world --foo-bar aiueo", expected: "Hello World! aiueo"); + } +} From d0f6066e4397bb7740cc9187762182e7fe594bb5 Mon Sep 17 00:00:00 2001 From: neuecc Date: Wed, 29 May 2024 20:50:56 +0900 Subject: [PATCH 36/54] working helps --- sandbox/GeneratorSandbox/Program.cs | 5 ++++ .../CommandHelpBuilder.cs | 8 +++++++ .../ConsoleAppGenerator.cs | 12 +++++++++- src/ConsoleAppFramework5/Emitter.cs | 23 +++++++++++++++++++ 4 files changed, 47 insertions(+), 1 deletion(-) diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index 65ba270..14de13f 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -25,6 +25,11 @@ ConsoleApp.Run(args, (int x, bool tako, int y = 999) => { }); + + + + + public class MyClass333 { public void HelloWorld(string fooBar) diff --git a/src/ConsoleAppFramework5/CommandHelpBuilder.cs b/src/ConsoleAppFramework5/CommandHelpBuilder.cs index 43933ab..1253dfb 100644 --- a/src/ConsoleAppFramework5/CommandHelpBuilder.cs +++ b/src/ConsoleAppFramework5/CommandHelpBuilder.cs @@ -38,6 +38,14 @@ public string BuildHelpMessage(Command command) return BuildHelpMessage(CreateCommandHelpDefinition(command, false), showCommandName: false, fromMultiCommand: false); } + public string BuildHelpMessage(Command[] commands) + { + // TODO: + return ""; + // return BuildHelpMessage(CreateCommandHelpDefinition(command, false), showCommandName: false, fromMultiCommand: false); + } + + internal string BuildHelpMessage(CommandHelpDefinition definition, bool showCommandName, bool fromMultiCommand) { var sb = new StringBuilder(); diff --git a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs index 7659773..6a5eac9 100644 --- a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs @@ -627,7 +627,17 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex var emitter = new Emitter(wellKnownTypes); emitter.EmitBuilder(sb, commands!, hasRun, hasRunAsync); } - sourceProductionContext.AddSource("ConsoleApp.Builder.g.cs", sb.ToString()); + + // Build Help + + var help = new SourceBuilder(0); + help.AppendLine(GeneratedCodeHeader); + using (help.BeginBlock("internal static partial class ConsoleApp")) + { + var emitter = new Emitter(wellKnownTypes); + emitter.EmitHelp(help, commands!); + } + sourceProductionContext.AddSource("ConsoleApp.Help.g.cs", help.ToString()); } } \ No newline at end of file diff --git a/src/ConsoleAppFramework5/Emitter.cs b/src/ConsoleAppFramework5/Emitter.cs index fc0d571..840c131 100644 --- a/src/ConsoleAppFramework5/Emitter.cs +++ b/src/ConsoleAppFramework5/Emitter.cs @@ -522,6 +522,7 @@ void EmitFilterInvoker(CommandWithId command) } } + // for single root command(Run) public void EmitHelp(SourceBuilder sb, Command command) { var helpBuilder = new CommandHelpBuilder(); @@ -534,5 +535,27 @@ public void EmitHelp(SourceBuilder sb, Command command) } } + public void EmitHelp(SourceBuilder sb, Command[] commands) + { + // only root + if (commands.Length == 1 && commands[0].IsRootCommand) + { + EmitHelp(sb, commands[0]); + return; + } + + var helpBuilder = new CommandHelpBuilder(); + + using (sb.BeginBlock("static partial void ShowHelp(int helpId)")) + { + // TODO: + // var help = helpBuilder.BuildHelpMessage(command); + + sb.AppendLine("Log(\"\"\""); + // sb.AppendLineWithoutIndent(help); + sb.AppendLineWithoutIndent("\"\"\");"); + } + } + internal record CommandWithId(string? FieldType, Command Command, int Id); } From 20f26a6b10140e7903546465e6fcb19f99e10874 Mon Sep 17 00:00:00 2001 From: neuecc Date: Thu, 30 May 2024 21:03:41 +0900 Subject: [PATCH 37/54] help builder --- sandbox/CliFrameworkBenchmark/Benchmark.cs | 2 +- sandbox/GeneratorSandbox/Program.cs | 17 +- src/ConsoleAppFramework5/Command.cs | 30 +- .../CommandHelpBuilder.cs | 281 ++++++++++-------- .../ConsoleAppGenerator.cs | 62 +++- .../DiagnosticDescriptors.cs | 2 +- src/ConsoleAppFramework5/Emitter.cs | 75 +++-- src/ConsoleAppFramework5/Parser.cs | 11 +- src/ConsoleAppFramework5/SourceBuilder.cs | 5 + .../ConsoleAppBuilderTest.cs | 16 +- .../DiagnosticsTest.cs | 26 +- .../FilterTest.cs | 16 +- .../HelpTest.cs | 247 +++++++++++++++ .../NameConverterTest.cs | 6 +- .../SubCommandTest.cs | 8 +- 15 files changed, 592 insertions(+), 212 deletions(-) create mode 100644 tests/ConsoleAppFramework.GeneratorTests/HelpTest.cs diff --git a/sandbox/CliFrameworkBenchmark/Benchmark.cs b/sandbox/CliFrameworkBenchmark/Benchmark.cs index 7eac03f..be4769c 100644 --- a/sandbox/CliFrameworkBenchmark/Benchmark.cs +++ b/sandbox/CliFrameworkBenchmark/Benchmark.cs @@ -87,7 +87,7 @@ public unsafe void ExecuteConsoleAppFramework() //[Benchmark(Description = "ConsoleAppFramework.Builder")] //public unsafe void ExecuteConsoleAppFrameworkBuilder() //{ - // var builder = ConsoleApp.CreateBuilder(); + // var builder = ConsoleApp.Create(); // builder.Add("", ConsoleAppFrameworkCommand.Execute); // builder.Run(TempArguments); //} diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index 14de13f..521de59 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -20,11 +20,12 @@ using static ConsoleAppFramework.ConsoleApp; -// args = ["do"]; // test. -//args = ["--foo-bar", "aiueo"]; - -ConsoleApp.Run(args, (int x, bool tako, int y = 999) => { }); +ConsoleApp.Run(args, (int xxx = 100, Fruit myFruit = Fruit.Apple) => { }); +enum Fruit +{ + Orange, Grape, Apple +} @@ -32,12 +33,14 @@ public class MyClass333 { + /// + /// -f|-fb, hello my world. + /// + /// public void HelloWorld(string fooBar) { Console.Write("Hello World! " + fooBar); } - - } @@ -401,7 +404,7 @@ partial struct ConsoleAppBuilder { - // public void AddFilter() where T : ConsoleAppFilter { } + // public void UseFilter() where T : ConsoleAppFilter { } //public class ConsoleAppBuilder diff --git a/src/ConsoleAppFramework5/Command.cs b/src/ConsoleAppFramework5/Command.cs index 50f6433..6b0e9e1 100644 --- a/src/ConsoleAppFramework5/Command.cs +++ b/src/ConsoleAppFramework5/Command.cs @@ -152,6 +152,7 @@ public record class CommandParameter public required bool IsNullableReference { get; init; } public required bool IsParams { get; init; } public required string Name { get; init; } + public required string OriginalParameterName { get; init; } public required bool HasDefaultValue { get; init; } public object? DefaultValue { get; init; } public required ITypeSymbol? CustomParserType { get; init; } @@ -270,7 +271,7 @@ string Core(ITypeSymbol type, bool nullable) } } - public string DefaultValueToString(bool castValue = true) + public string DefaultValueToString(bool castValue = true, bool enumIncludeTypeName = true) { if (DefaultValue is bool b) { @@ -285,17 +286,42 @@ public string DefaultValueToString(bool castValue = true) // null -> default(T) to support both class and struct return $"default({Type.ToFullyQualifiedFormatDisplayString()})"; } + if (Type.TypeKind == TypeKind.Enum) + { + var symbol = Type.GetMembers().OfType().FirstOrDefault(x => object.Equals(x.ConstantValue, DefaultValue)); + if (symbol == null) + { + return $"default({Type.ToFullyQualifiedFormatDisplayString()})"; + } + else + { + return enumIncludeTypeName ? $"{Type.ToFullyQualifiedFormatDisplayString()}.{symbol.Name}" : symbol.Name; + } + } if (!castValue) return DefaultValue.ToString(); return $"({Type.ToFullyQualifiedFormatDisplayString()}){DefaultValue}"; } + public string? GetEnumSymbolName(object value) + { + var symbol = Type.GetMembers().OfType().FirstOrDefault(x => x.ConstantValue == value); + if (symbol == null) return ""; + return symbol.Name; + } + public string ToTypeDisplayString() { var t = Type.ToFullyQualifiedFormatDisplayString(); return IsNullableReference ? $"{t}?" : t; } + public string ToTypeShortString() + { + var t = Type.ToDisplayString(NullableFlowState.NotNull, SymbolDisplayFormat.MinimallyQualifiedFormat); + return IsNullableReference ? $"{t}?" : t; + } + public override string ToString() { var sb = new StringBuilder(); @@ -305,7 +331,7 @@ public override string ToString() } sb.Append(ToTypeDisplayString()); sb.Append(" "); - sb.Append(Name); + sb.Append(OriginalParameterName); if (HasDefaultValue) { sb.Append(" = "); diff --git a/src/ConsoleAppFramework5/CommandHelpBuilder.cs b/src/ConsoleAppFramework5/CommandHelpBuilder.cs index 1253dfb..cf4e72d 100644 --- a/src/ConsoleAppFramework5/CommandHelpBuilder.cs +++ b/src/ConsoleAppFramework5/CommandHelpBuilder.cs @@ -3,98 +3,103 @@ namespace ConsoleAppFramework; -public class CommandHelpBuilder +public static class CommandHelpBuilder { - //public string BuildHelpMessage(CommandDescriptor? defaultCommand, IEnumerable commands, bool shortCommandName) - //{ - // var sb = new StringBuilder(); - - // bool showHeader = (defaultCommand != null); - // if (defaultCommand != null) - // { - // // Display a help messages for default method - // sb.Append(BuildHelpMessage(CreateCommandHelpDefinition(defaultCommand, shortCommandName), showCommandName: false, fromMultiCommand: false)); - // } - - // var orderedCommands = options.HelpSortCommandsByFullName - // ? commands.OrderBy(x => x.GetCommandName(options)).ToArray() - // : commands.OrderBy(x => x.GetNamesFormatted(options)).ToArray(); - // if (orderedCommands.Length > 0) - // { - // if (defaultCommand == null) - // { - // sb.Append(BuildUsageMessage()); - // sb.AppendLine(); - // } - - // sb.Append(BuildMethodListMessage(orderedCommands, shortCommandName, out var maxWidth)); - // } - - // return sb.ToString(); - //} - - public string BuildHelpMessage(Command command) + public static string BuildRootHelpMessage(Command command) { - return BuildHelpMessage(CreateCommandHelpDefinition(command, false), showCommandName: false, fromMultiCommand: false); + return BuildHelpMessageCore(command, showCommandName: false, showCommand: false); } - public string BuildHelpMessage(Command[] commands) - { - // TODO: - return ""; - // return BuildHelpMessage(CreateCommandHelpDefinition(command, false), showCommandName: false, fromMultiCommand: false); - } - - - internal string BuildHelpMessage(CommandHelpDefinition definition, bool showCommandName, bool fromMultiCommand) + public static string BuildRootHelpMessage(Command[] commands) { var sb = new StringBuilder(); - sb.AppendLine(BuildUsageMessage(definition, showCommandName, fromMultiCommand)); - sb.AppendLine(); + var rootCommand = commands.FirstOrDefault(x => x.IsRootCommand); + var withoutRoot = commands.Where(x => !x.IsRootCommand).ToArray(); - if (!string.IsNullOrEmpty(definition.Description)) + if (rootCommand != null && withoutRoot.Length == 0) { - sb.AppendLine(definition.Description); - sb.AppendLine(); + return BuildRootHelpMessage(commands[0]); } - if (definition.Options.Any()) + if (rootCommand != null) { - sb.Append(BuildArgumentsMessage(definition)); - sb.Append(BuildOptionsMessage(definition)); + sb.AppendLine(BuildHelpMessageCore(rootCommand, false, withoutRoot.Length != 0)); } else { - sb.AppendLine("Options:"); - sb.AppendLine(" ()"); + sb.AppendLine("Usage: [command] [-h|--help] [--version]"); sb.AppendLine(); } + if (withoutRoot.Length == 0) return sb.ToString(); + + var helpDefinitions = withoutRoot.OrderBy(x => x.CommandFullName).ToArray(); + + var list = BuildMethodListMessage(helpDefinitions, out _); + sb.Append(list); + return sb.ToString(); } - internal string BuildUsageMessage() + public static string BuildCommandHelpMessage(Command command) { + return BuildHelpMessageCore(command, showCommandName: command.CommandName != "", showCommand: false); + } + + static string BuildHelpMessageCore(Command command, bool showCommandName, bool showCommand) + { + var definition = CreateCommandHelpDefinition(command); + var sb = new StringBuilder(); - sb.AppendLine($"Usage: "); + + sb.AppendLine(BuildUsageMessage(definition, showCommandName, showCommand)); + + if (!string.IsNullOrEmpty(definition.Description)) + { + sb.AppendLine(); + sb.AppendLine(definition.Description); + } + + if (definition.Options.Any()) + { + var hasArgument = definition.Options.Any(x => x.Index.HasValue); + var hasOptions = definition.Options.Any(x => !x.Index.HasValue); + + if (hasArgument) + { + sb.AppendLine(); + sb.AppendLine(BuildArgumentsMessage(definition)); + } + + if (hasOptions) + { + sb.AppendLine(); + sb.AppendLine(BuildOptionsMessage(definition)); + } + } return sb.ToString(); } - internal string BuildUsageMessage(CommandHelpDefinition definition, bool showCommandName, bool fromMultiCommand) + static string BuildUsageMessage(CommandHelpDefinition definition, bool showCommandName, bool showCommand) { var sb = new StringBuilder(); sb.Append($"Usage:"); if (showCommandName) { - sb.Append($" {definition.Command}"); + sb.Append($" {definition.CommandName}"); + } + + if (showCommand) + { + sb.Append(" [command]"); } - foreach (var opt in definition.Options.Where(x => x.Index.HasValue)) + if (definition.Options.Any(x => x.Index.HasValue)) { - sb.Append($" <{(string.IsNullOrEmpty(opt.Description) ? opt.Options[0] : opt.Description)}>"); + sb.Append(" [arguments...]"); } if (definition.Options.Any(x => !x.Index.HasValue)) @@ -102,10 +107,12 @@ internal string BuildUsageMessage(CommandHelpDefinition definition, bool showCom sb.Append(" [options...]"); } + sb.Append(" [-h|--help] [--version]"); + return sb.ToString(); } - internal string BuildArgumentsMessage(CommandHelpDefinition definition) + static string BuildArgumentsMessage(CommandHelpDefinition definition) { var argumentsFormatted = definition.Options .Where(x => x.Index.HasValue) @@ -119,31 +126,41 @@ internal string BuildArgumentsMessage(CommandHelpDefinition definition) var sb = new StringBuilder(); sb.AppendLine("Arguments:"); + var first = true; foreach (var arg in argumentsFormatted) { + if (first) + { + first = false; + } + else + { + sb.AppendLine(); + } var padding = maxWidth - arg.Argument.Length; sb.Append(" "); sb.Append(arg.Argument); - for (var i = 0; i < padding; i++) + if (!string.IsNullOrEmpty(arg.Description)) { - sb.Append(' '); - } + for (var i = 0; i < padding; i++) + { + sb.Append(' '); + } - sb.Append(" "); - sb.AppendLine(arg.Description); + sb.Append(" "); + sb.Append(arg.Description); + } } - sb.AppendLine(); - return sb.ToString(); } - internal string BuildOptionsMessage(CommandHelpDefinition definition) + static string BuildOptionsMessage(CommandHelpDefinition definition) { var optionsFormatted = definition.Options .Where(x => !x.Index.HasValue) - .Select(x => (Options: string.Join(", ", x.Options) + (x.IsFlag ? string.Empty : $" {x.FormattedValueTypeName}"), x.Description, x.IsRequired, x.IsFlag, x.DefaultValue)) + .Select(x => (Options: string.Join("|", x.Options) + (x.IsFlag ? string.Empty : $" {x.FormattedValueTypeName}{(x.IsParams ? "..." : "")}"), x.Description, x.IsRequired, x.IsFlag, x.DefaultValue)) .ToArray(); if (!optionsFormatted.Any()) return string.Empty; @@ -153,8 +170,18 @@ internal string BuildOptionsMessage(CommandHelpDefinition definition) var sb = new StringBuilder(); sb.AppendLine("Options:"); + var first = true; foreach (var opt in optionsFormatted) { + if (first) + { + first = false; + } + else + { + sb.AppendLine(); + } + var options = opt.Options; var padding = maxWidth - options.Length; @@ -180,50 +207,55 @@ internal string BuildOptionsMessage(CommandHelpDefinition definition) { sb.Append($" (Required)"); } - - sb.AppendLine(); } - sb.AppendLine(); + return sb.ToString(); + } + + static string BuildMethodListMessage(IEnumerable commands, out int maxWidth) + { + var formatted = commands + .Select(x => + { + var full = x.CommandName; + if (x.CommandPath.Length > 0) + { + full = string.Join(" ", x.CommandPath) + " " + x.CommandName; + } + + return (Command: full, x.Description); + }) + .ToArray(); + maxWidth = formatted.Max(x => x.Command.Length); + + var sb = new StringBuilder(); + + sb.AppendLine("Commands:"); + foreach (var item in formatted) + { + sb.Append(" "); + sb.Append(item.Command); + if (string.IsNullOrEmpty(item.Description)) + { + sb.AppendLine(); + } + else + { + var padding = maxWidth - item.Command.Length; + for (var i = 0; i < padding; i++) + { + sb.Append(' '); + } + + sb.Append(" "); + sb.AppendLine(item.Description); + } + } return sb.ToString(); } - //internal string BuildMethodListMessage(IEnumerable types, bool shortCommandName, out int maxWidth) - //{ - // maxWidth = 0; - // return BuildMethodListMessage(types.Select(x => CreateCommandHelpDefinition(x, shortCommandName)), true, out maxWidth); - //} - - //internal string BuildMethodListMessage(IEnumerable commandHelpDefinitions, bool appendCommand, out int maxWidth) - //{ - // var formatted = commandHelpDefinitions - // .Select(x => (Command: $"{(x.CommandAliases.Length != 0 ? ((appendCommand ? x.Command + " " : "") + string.Join(", ", x.CommandAliases)) : x.Command)}", Description: x.Description)) - // .ToArray(); - // maxWidth = formatted.Max(x => x.Command.Length); - - // var sb = new StringBuilder(); - - // sb.AppendLine("Commands:"); - // foreach (var item in formatted) - // { - // sb.Append(" "); - // sb.Append(item.Command); - - // var padding = maxWidth - item.Command.Length; - // for (var i = 0; i < padding; i++) - // { - // sb.Append(' '); - // } - - // sb.Append(" "); - // sb.AppendLine(item.Description); - // } - - // return sb.ToString(); - //} - - internal CommandHelpDefinition CreateCommandHelpDefinition(Command descriptor, bool shortCommandName) + static CommandHelpDefinition CreateCommandHelpDefinition(Command descriptor) { var parameterDefinitions = new List(); @@ -242,20 +274,22 @@ internal CommandHelpDefinition CreateCommandHelpDefinition(Command descriptor, b } else { - options.Add("--" + item.Name); + // aliases first foreach (var alias in item.Aliases) { options.Add(alias); } + options.Add("--" + item.Name); } var description = item.Description; var isFlag = item.Type.SpecialType == Microsoft.CodeAnalysis.SpecialType.System_Boolean; + var isParams = item.IsParams; var defaultValue = default(string); if (item.HasDefaultValue) { - defaultValue = (item.DefaultValue?.ToString() ?? "null"); + defaultValue = item.DefaultValue == null ? "null" : item.DefaultValueToString(castValue: false, enumIncludeTypeName: false); if (isFlag) { if (item.DefaultValue is true) @@ -271,41 +305,38 @@ internal CommandHelpDefinition CreateCommandHelpDefinition(Command descriptor, b } } - var paramTypeName = item.ToTypeDisplayString(); - if (item.Type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) - { - paramTypeName = ((item.Type as INamedTypeSymbol)!.TypeArguments[0]).ToFullyQualifiedFormatDisplayString() + "?"; - } + var paramTypeName = item.ToTypeShortString(); + parameterDefinitions.Add(new CommandOptionHelpDefinition(options.Distinct().ToArray(), description, paramTypeName, defaultValue, index, isFlag, isParams)); + } - parameterDefinitions.Add(new CommandOptionHelpDefinition(options.Distinct().ToArray(), description, paramTypeName, defaultValue, index, isFlag)); + var commandName = descriptor.CommandName; + if (descriptor.CommandPath.Length != 0) + { + commandName = string.Join(" ", descriptor.CommandPath) + " " + descriptor.CommandName; } return new CommandHelpDefinition( - descriptor.CommandName, - // descriptor.Aliases, + commandName, parameterDefinitions.ToArray(), descriptor.Description ); } - public class CommandHelpDefinition + class CommandHelpDefinition { - // TODO: Command Path - - public string Command { get; } + public string CommandName { get; } public CommandOptionHelpDefinition[] Options { get; } public string Description { get; } public CommandHelpDefinition(string command, CommandOptionHelpDefinition[] options, string description) { - Command = command; + CommandName = command; Options = options; Description = description; } } - // TODO: params? - public class CommandOptionHelpDefinition + class CommandOptionHelpDefinition { public string[] Options { get; } public string Description { get; } @@ -313,11 +344,12 @@ public class CommandOptionHelpDefinition public string ValueTypeName { get; } public int? Index { get; } - public bool IsRequired => DefaultValue == null; + public bool IsRequired => DefaultValue == null && !IsParams; public bool IsFlag { get; } + public bool IsParams { get; } public string FormattedValueTypeName => "<" + ValueTypeName + ">"; - public CommandOptionHelpDefinition(string[] options, string description, string valueTypeName, string? defaultValue, int? index, bool isFlag) + public CommandOptionHelpDefinition(string[] options, string description, string valueTypeName, string? defaultValue, int? index, bool isFlag, bool isParams) { Options = options; Description = description; @@ -325,6 +357,7 @@ public CommandOptionHelpDefinition(string[] options, string description, string DefaultValue = defaultValue; Index = index; IsFlag = isFlag; + IsParams = isParams; } } -} +} \ No newline at end of file diff --git a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs index 6a5eac9..760f057 100644 --- a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs @@ -5,6 +5,7 @@ using System.ComponentModel.Design; using System.Reflection; using System.Xml.Linq; +using static ConsoleAppFramework.Emitter; namespace ConsoleAppFramework; @@ -52,7 +53,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var expr = invocationExpression.Expression as MemberAccessExpressionSyntax; var methodName = expr?.Name.Identifier.Text; - if (methodName is "Add" or "AddFilter" or "Run" or "RunAsync") + if (methodName is "Add" or "UseFilter" or "Run" or "RunAsync") { return true; } @@ -156,7 +157,7 @@ public static Task RunAsync(string[] args) return Task.CompletedTask; } - public static ConsoleAppBuilder CreateBuilder() => new ConsoleAppBuilder(); + public static ConsoleAppBuilder Create() => new ConsoleAppBuilder(); static void ThrowArgumentParseFailed(string argumentName, string value) { @@ -410,7 +411,7 @@ public void Add() { } public void Add(string commandPath) { } [System.Diagnostics.Conditional("DEBUG")] - public void AddFilter() where T : ConsoleAppFilter { } + public void UseFilter() where T : ConsoleAppFilter { } public void Run(string[] args) { @@ -432,6 +433,38 @@ public Task RunAsync(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 parameterCount, int helpId) + { + if (args.Length == 0) + { + if (parameterCount == 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; + } } } """; @@ -512,7 +545,7 @@ static void EmitConsoleAppRun(SourceProductionContext sourceProductionContext, ( var emitter = new Emitter(wellKnownTypes); emitter.EmitHelp(help, command); } - sourceProductionContext.AddSource("ConsoleApp.Help.g.cs", help.ToString()); + sourceProductionContext.AddSource("ConsoleApp.Run.Help.g.cs", help.ToString()); } static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContext, ImmutableArray<(InvocationExpressionSyntax Node, string Name, SemanticModel Model)> generatorSyntaxContexts) @@ -547,7 +580,7 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex return x.Name; }); - var globalFilters = methodGroup["AddFilter"] + var globalFilters = methodGroup["UseFilter"] .Select(x => { var genericName = (x.Node.Expression as MemberAccessExpressionSyntax)?.Name as GenericNameSyntax; @@ -622,10 +655,22 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex var sb = new SourceBuilder(0); sb.AppendLine(GeneratedCodeHeader); + // with id number + var commandIds = commands + .Select((x, i) => + { + return new CommandWithId( + FieldType: x!.BuildDelegateSignature(out _), // for builder, always generate Action/Func so ok to ignore out var. + Command: x!, + Id: i + ); + }) + .ToArray(); + using (sb.BeginBlock("internal static partial class ConsoleApp")) { var emitter = new Emitter(wellKnownTypes); - emitter.EmitBuilder(sb, commands!, hasRun, hasRunAsync); + emitter.EmitBuilder(sb, commandIds, hasRun, hasRunAsync); } sourceProductionContext.AddSource("ConsoleApp.Builder.g.cs", sb.ToString()); @@ -634,10 +679,11 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex var help = new SourceBuilder(0); help.AppendLine(GeneratedCodeHeader); using (help.BeginBlock("internal static partial class ConsoleApp")) + using (help.BeginBlock("internal partial struct ConsoleAppBuilder")) { var emitter = new Emitter(wellKnownTypes); - emitter.EmitHelp(help, commands!); + emitter.EmitHelp(help, commandIds!); } - sourceProductionContext.AddSource("ConsoleApp.Help.g.cs", help.ToString()); + sourceProductionContext.AddSource("ConsoleApp.Builder.Help.g.cs", help.ToString()); } } \ No newline at end of file diff --git a/src/ConsoleAppFramework5/DiagnosticDescriptors.cs b/src/ConsoleAppFramework5/DiagnosticDescriptors.cs index 774e9de..7706038 100644 --- a/src/ConsoleAppFramework5/DiagnosticDescriptors.cs +++ b/src/ConsoleAppFramework5/DiagnosticDescriptors.cs @@ -61,7 +61,7 @@ public static DiagnosticDescriptor Create(int id, string title, string messageFo public static DiagnosticDescriptor AddInLoopIsNotAllowed { get; } = Create( 8, - "ConsoleAppBuilder.Add/AddFilter is not allowed in loop statements(while, do, for, foreach)."); + "ConsoleAppBuilder.Add/UseFilter is not allowed in loop statements(while, do, for, foreach)."); public static DiagnosticDescriptor CommandHasFilter { get; } = Create( 9, diff --git a/src/ConsoleAppFramework5/Emitter.cs b/src/ConsoleAppFramework5/Emitter.cs index 840c131..617fe95 100644 --- a/src/ConsoleAppFramework5/Emitter.cs +++ b/src/ConsoleAppFramework5/Emitter.cs @@ -287,22 +287,11 @@ public void EmitRun(SourceBuilder sb, CommandWithId commandWithId, bool isRunAsy } } - public void EmitBuilder(SourceBuilder sb, Command[] commands, bool emitSync, bool emitAsync) + public void EmitBuilder(SourceBuilder sb, CommandWithId[] commandIds, bool emitSync, bool emitAsync) { - // with id number - var commandIds = commands - .Select((x, i) => - { - return new CommandWithId( - FieldType: x.BuildDelegateSignature(out _), // for builder, always generate Action/Func so ok to ignore out var. - Command: x, - Id: i - ); - }) - .ToArray(); - // grouped by path var commandGroup = commandIds.ToLookup(x => x.Command.CommandPath.Length == 0 ? x.Command.CommandName : x.Command.CommandPath[0]); + var hasRootCommand = commandIds.Any(x => x.Command.IsRootCommand); using (sb.BeginBlock("partial struct ConsoleAppBuilder")) { @@ -339,6 +328,15 @@ public void EmitBuilder(SourceBuilder sb, Command[] commands, bool emitSync, boo sb.AppendLine(); using (sb.BeginBlock("partial void RunCore(string[] args)")) { + if (hasRootCommand) + { + using (sb.BeginBlock("if (args.Length == 1 && args[0] is \"--help\" or \"-h\")")) + { + sb.AppendLine("ShowHelp(-1);"); + sb.AppendLine("return;"); + } + } + EmitRunBody(commandGroup, 0, false); } } @@ -349,6 +347,15 @@ public void EmitBuilder(SourceBuilder sb, Command[] commands, bool emitSync, boo sb.AppendLine(); using (sb.BeginBlock("partial void RunAsyncCore(string[] args, ref Task result)")) { + if (hasRootCommand) + { + using (sb.BeginBlock("if (args.Length == 1 && args[0] is \"--help\" or \"-h\")")) + { + sb.AppendLine("ShowHelp(-1);"); + sb.AppendLine("return;"); + } + } + EmitRunBody(commandGroup, 0, true); } } @@ -456,7 +463,7 @@ void EmitLeafCommand(CommandWithId? command) { if (command == null) { - sb.AppendLine("TryShowHelpOrVersion(args, -1, -1);"); + sb.AppendLine("ShowHelp(-1);"); } else { @@ -525,35 +532,39 @@ void EmitFilterInvoker(CommandWithId command) // for single root command(Run) public void EmitHelp(SourceBuilder sb, Command command) { - var helpBuilder = new CommandHelpBuilder(); - var help = helpBuilder.BuildHelpMessage(command); using (sb.BeginBlock("static partial void ShowHelp(int helpId)")) { sb.AppendLine("Log(\"\"\""); - sb.AppendLineWithoutIndent(help); + sb.AppendWithoutIndent(CommandHelpBuilder.BuildRootHelpMessage(command)); sb.AppendLineWithoutIndent("\"\"\");"); } } - public void EmitHelp(SourceBuilder sb, Command[] commands) + public void EmitHelp(SourceBuilder sb, CommandWithId[] commands) { - // only root - if (commands.Length == 1 && commands[0].IsRootCommand) - { - EmitHelp(sb, commands[0]); - return; - } - - var helpBuilder = new CommandHelpBuilder(); - using (sb.BeginBlock("static partial void ShowHelp(int helpId)")) { - // TODO: - // var help = helpBuilder.BuildHelpMessage(command); + using (sb.BeginBlock("switch (helpId)")) + { + foreach (var command in commands) + { + using (sb.BeginIndent($"case {command.Id}:")) + { + sb.AppendLine("Log(\"\"\""); + sb.AppendWithoutIndent(CommandHelpBuilder.BuildCommandHelpMessage(command.Command)); + sb.AppendLineWithoutIndent("\"\"\");"); + sb.AppendLine("break;"); + } + } - sb.AppendLine("Log(\"\"\""); - // sb.AppendLineWithoutIndent(help); - sb.AppendLineWithoutIndent("\"\"\");"); + using (sb.BeginIndent("default:")) + { + sb.AppendLine("Log(\"\"\""); + sb.AppendWithoutIndent(CommandHelpBuilder.BuildRootHelpMessage(commands.Select(x => x.Command).ToArray())); + sb.AppendLineWithoutIndent("\"\"\");"); + sb.AppendLine("break;"); + } + } } } diff --git a/src/ConsoleAppFramework5/Parser.cs b/src/ConsoleAppFramework5/Parser.cs index 4f45112..2f3a782 100644 --- a/src/ConsoleAppFramework5/Parser.cs +++ b/src/ConsoleAppFramework5/Parser.cs @@ -54,7 +54,6 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta return null; } - context.ReportDiagnostic(DiagnosticDescriptors.RequireArgsAndMethod, node.GetLocation()); return null; } @@ -269,6 +268,14 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta defaultValue = token.Value; } } + else if (x.Default != null) + { + var value = model.GetConstantValue(x.Default.Value); + if (value.HasValue) + { + defaultValue = value.Value; + } + } // bool is always optional flag if (type.Type?.SpecialType == SpecialType.System_Boolean) @@ -348,6 +355,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta return new CommandParameter { Name = NameConverter.ToKebabCase(x.Identifier.Text), + OriginalParameterName = x.Identifier.Text, IsNullableReference = isNullableReference, IsParams = hasParams, Type = type.Type!, @@ -501,6 +509,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta return new CommandParameter { Name = NameConverter.ToKebabCase(x.Name), + OriginalParameterName = x.Name, IsNullableReference = isNullableReference, IsParams = x.IsParams, Location = x.DeclaringSyntaxReferences[0].GetSyntax().GetLocation(), diff --git a/src/ConsoleAppFramework5/SourceBuilder.cs b/src/ConsoleAppFramework5/SourceBuilder.cs index 0c1f297..d62e318 100644 --- a/src/ConsoleAppFramework5/SourceBuilder.cs +++ b/src/ConsoleAppFramework5/SourceBuilder.cs @@ -71,6 +71,11 @@ public void AppendLine(string text) builder.AppendLine(text); } + public void AppendWithoutIndent(string text) + { + builder.Append(text); + } + public void AppendLineWithoutIndent(string text) { builder.AppendLine(text); diff --git a/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppBuilderTest.cs b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppBuilderTest.cs index d5cf2ed..21c0c83 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppBuilderTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppBuilderTest.cs @@ -16,7 +16,7 @@ public class ConsoleAppBuilderTest(ITestOutputHelper output) public void BuilderRun() { var code = """ -var builder = ConsoleApp.CreateBuilder(); +var builder = ConsoleApp.Create(); builder.Add("foo", (int x, int y) => { Console.Write(x + y); }); builder.Add("bar", (int x, int y = 10) => { Console.Write(x + y); }); builder.Add("baz", int (int x, string y) => { Console.Write(x + y); return 10; }); @@ -39,7 +39,7 @@ public void BuilderRun() public void BuilderRunAsync() { var code = """ -var builder = ConsoleApp.CreateBuilder(); +var builder = ConsoleApp.Create(); builder.Add("foo", (int x, int y) => { Console.Write(x + y); }); builder.Add("bar", (int x, int y = 10) => { Console.Write(x + y); }); builder.Add("baz", int (int x, string y) => { Console.Write(x + y); return 10; }); @@ -62,7 +62,7 @@ public void BuilderRunAsync() public void AddClass() { var code = """ -var builder = ConsoleApp.CreateBuilder(); +var builder = ConsoleApp.Create(); builder.Add(); await builder.RunAsync(args); @@ -102,7 +102,7 @@ public static void Sum() public void ClassDispose() { verifier.Execute(""" -var builder = ConsoleApp.CreateBuilder(); +var builder = ConsoleApp.Create(); builder.Add(); builder.Run(args); @@ -121,7 +121,7 @@ public void Dispose() """, "do", "yeah:disposed!"); verifier.Execute(""" -var builder = ConsoleApp.CreateBuilder(); +var builder = ConsoleApp.Create(); builder.Add(); await builder.RunAsync(args); @@ -140,7 +140,7 @@ public void Dispose() """, "do", "yeah:disposed!"); verifier.Execute(""" -var builder = ConsoleApp.CreateBuilder(); +var builder = ConsoleApp.Create(); builder.Add(); await builder.RunAsync(args); @@ -169,7 +169,7 @@ public void ClassWithDI() serviceCollection.Register(typeof(int), 9999); ConsoleApp.ServiceProvider = serviceCollection; -var builder = ConsoleApp.CreateBuilder(); +var builder = ConsoleApp.Create(); builder.Add(); builder.Run(args); @@ -204,7 +204,7 @@ public object GetService(Type serviceType) public void CommandAttr() { var code = """ -var builder = ConsoleApp.CreateBuilder(); +var builder = ConsoleApp.Create(); builder.Add(); builder.Run(args); diff --git a/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs b/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs index d0ed2e6..18abb21 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs @@ -91,13 +91,13 @@ public void FunctionPointerValidation() public void BuilderAddConstCommandName() { verifier.Verify(6, """ -var builder = ConsoleApp.CreateBuilder(); +var builder = ConsoleApp.Create(); var baz = "foo"; builder.Add(baz, (int x, int y) => { } ); """, "baz"); verifier.Ok(""" -var builder = ConsoleApp.CreateBuilder(); +var builder = ConsoleApp.Create(); builder.Add("foo", (int x, int y) => { } ); builder.Run(args); """); @@ -107,7 +107,7 @@ public void BuilderAddConstCommandName() public void DuplicateCommandName() { verifier.Verify(7, """ -var builder = ConsoleApp.CreateBuilder(); +var builder = ConsoleApp.Create(); builder.Add("foo", (int x, int y) => { } ); builder.Add("foo", (int x, int y) => { } ); """, "\"foo\""); @@ -117,7 +117,7 @@ public void DuplicateCommandName() public void DuplicateCommandNameClass() { verifier.Verify(7, """ -var builder = ConsoleApp.CreateBuilder(); +var builder = ConsoleApp.Create(); builder.Add(); public class MyClass @@ -135,7 +135,7 @@ public void Do(int i) """, "builder.Add()"); verifier.Verify(7, """ -var builder = ConsoleApp.CreateBuilder(); +var builder = ConsoleApp.Create(); builder.Add("do", (int x, int y) => { } ); builder.Add(); builder.Run(args); @@ -163,7 +163,7 @@ public void Do() } """; verifier.Verify(8, $$""" -var builder = ConsoleApp.CreateBuilder(); +var builder = ConsoleApp.Create(); while (true) { builder.Add(); @@ -173,7 +173,7 @@ public void Do() """, "builder.Add()"); verifier.Verify(8, $$""" -var builder = ConsoleApp.CreateBuilder(); +var builder = ConsoleApp.Create(); for (int i = 0; i < 10; i++) { builder.Add(); @@ -183,7 +183,7 @@ public void Do() """, "builder.Add()"); verifier.Verify(8, $$""" -var builder = ConsoleApp.CreateBuilder(); +var builder = ConsoleApp.Create(); do { builder.Add(); @@ -193,7 +193,7 @@ public void Do() """, "builder.Add()"); verifier.Verify(8, $$""" -var builder = ConsoleApp.CreateBuilder(); +var builder = ConsoleApp.Create(); foreach (var item in new[]{1,2,3}) { builder.Add(); @@ -207,7 +207,7 @@ public void Do() public void ErrorInBuilderAPI() { verifier.Verify(3, $$""" -var builder = ConsoleApp.CreateBuilder(); +var builder = ConsoleApp.Create(); builder.Add(); public class MyClass @@ -221,7 +221,7 @@ public string Do() """, "string"); verifier.Verify(3, $$""" -var builder = ConsoleApp.CreateBuilder(); +var builder = ConsoleApp.Create(); builder.Add(); public class MyClass @@ -235,12 +235,12 @@ public async Task Do() """, "Task"); verifier.Verify(2, $$""" -var builder = ConsoleApp.CreateBuilder(); +var builder = ConsoleApp.Create(); builder.Add("foo", string (int x, int y) => { return "foo"; }); """, "string"); verifier.Verify(2, $$""" -var builder = ConsoleApp.CreateBuilder(); +var builder = ConsoleApp.Create(); builder.Add("foo", async Task (int x, int y) => { return "foo"; }); """, "Task"); } diff --git a/tests/ConsoleAppFramework.GeneratorTests/FilterTest.cs b/tests/ConsoleAppFramework.GeneratorTests/FilterTest.cs index 83d45a2..c650a03 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/FilterTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/FilterTest.cs @@ -15,10 +15,10 @@ public class FilterTest(ITestOutputHelper output) public void ForLambda() { verifier.Execute(""" -var builder = ConsoleApp.CreateBuilder(); +var builder = ConsoleApp.Create(); -builder.AddFilter(); -builder.AddFilter(); +builder.UseFilter(); +builder.UseFilter(); builder.Add("", Hello); @@ -77,10 +77,10 @@ public override Task InvokeAsync(CancellationToken cancellationToken) public void ForClass() { verifier.Execute(""" -var builder = ConsoleApp.CreateBuilder(); +var builder = ConsoleApp.Create(); -builder.AddFilter(); -builder.AddFilter(); +builder.UseFilter(); +builder.UseFilter(); builder.Add(); @@ -170,9 +170,9 @@ public void DI() serviceCollection.Register(typeof(int), 9999); ConsoleApp.ServiceProvider = serviceCollection; -var builder = ConsoleApp.CreateBuilder(); +var builder = ConsoleApp.Create(); -builder.AddFilter(); +builder.UseFilter(); builder.Add("", () => Console.Write("do")); diff --git a/tests/ConsoleAppFramework.GeneratorTests/HelpTest.cs b/tests/ConsoleAppFramework.GeneratorTests/HelpTest.cs new file mode 100644 index 0000000..a0f56ec --- /dev/null +++ b/tests/ConsoleAppFramework.GeneratorTests/HelpTest.cs @@ -0,0 +1,247 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit.Abstractions; + +namespace ConsoleAppFramework.GeneratorTests; + +public class HelpTest(ITestOutputHelper output) +{ + VerifyHelper verifier = new VerifyHelper(output, "CAF"); + + [Fact] + public void Run() + { + verifier.Execute(code: """ +ConsoleApp.Run(args, (int x, int y) => { }); +""", + args: "--help", + expected: """ +Usage: [options...] [-h|--help] [--version] + +Options: + --x (Required) + --y (Required) + +"""); + } + + [Fact] + public void RunVoid() + { + verifier.Execute(code: """ +ConsoleApp.Run(args, () => { }); +""", + args: "--help", + expected: """ +Usage: [-h|--help] [--version] + +"""); + } + + [Fact] + public void RootOnly() + { + verifier.Execute(code: """ +var app = ConsoleApp.Create(); +app.Add("", (int x, int y) => { }); +app.Run(args); +""", + args: "--help", + expected: """ +Usage: [options...] [-h|--help] [--version] + +Options: + --x (Required) + --y (Required) + +"""); + } + + [Fact] + public void ListWithoutRoot() + { + var code = """ +var app = ConsoleApp.Create(); +app.Add("a", (int x, int y) => { }); +app.Add("ab", (int x, int y) => { }); +app.Add("a/b/c", (int x, int y) => { }); +app.Run(args); +"""; + verifier.Execute(code, args: "--help", expected: """ +Usage: [command] [-h|--help] [--version] + +Commands: + a + a b c + ab + +"""); + } + + [Fact] + public void ListWithRoot() + { + var code = """ +var app = ConsoleApp.Create(); +app.Add("", (int x, int y) => { }); +app.Add("a", (int x, int y) => { }); +app.Add("ab", (int x, int y) => { }); +app.Add("a/b/c", (int x, int y) => { }); +app.Run(args); +"""; + verifier.Execute(code, args: "--help", expected: """ +Usage: [command] [options...] [-h|--help] [--version] + +Options: + --x (Required) + --y (Required) + +Commands: + a + a b c + ab + +"""); + } + + [Fact] + public void SelectLeafHelp() + { + var code = """ +var app = ConsoleApp.Create(); +app.Add("", (int x, int y) => { }); +app.Add("a", (int x, int y) => { }); +app.Add("ab", (int x, int y) => { }); +app.Add("a/b/c", (int x, int y) => { }); +app.Run(args); +"""; + verifier.Execute(code, args: "a b c --help", expected: """ +Usage: a b c [options...] [-h|--help] [--version] + +Options: + --x (Required) + --y (Required) + +"""); + } + + [Fact] + public void Summary() + { + var code = """ +var app = ConsoleApp.Create(); +app.Add(); +app.Run(args); + +public class MyClass +{ + /// + /// hello my world. + /// + /// -f|-fb, my foo is not bar. + public void HelloWorld(string fooBar) + { + Console.Write("Hello World! " + fooBar); + } +} +"""; + verifier.Execute(code, args: "--help", expected: """ +Usage: [command] [-h|--help] [--version] + +Commands: + hello-world hello my world. + +"""); + + verifier.Execute(code, args: "hello-world --help", expected: """ +Usage: hello-world [options...] [-h|--help] [--version] + +hello my world. + +Options: + -f|-fb|--foo-bar my foo is not bar. (Required) + +"""); + } + + [Fact] + public void ArgumentOnly() + { + verifier.Execute(code: """ +ConsoleApp.Run(args, ([Argument]int x, [Argument]int y) => { }); +""", + args: "--help", + expected: """ +Usage: [arguments...] [-h|--help] [--version] + +Arguments: + [0] + [1] + +"""); + } + + [Fact] + public void ArgumentWithParams() + { + verifier.Execute(code: """ +ConsoleApp.Run(args, ([Argument]int x, [Argument]int y, params string[] yyy) => { }); +""", + args: "--help", + expected: """ +Usage: [arguments...] [options...] [-h|--help] [--version] + +Arguments: + [0] + [1] + +Options: + --yyy ... + +"""); + } + + // Params + + [Fact] + public void Nullable() + { + verifier.Execute(code: """ +ConsoleApp.Run(args, (int? x = null, string? y = null) => { }); +""", + args: "--help", + expected: """ +Usage: [options...] [-h|--help] [--version] + +Options: + --x (Default: null) + --y (Default: null) + +"""); + } + + [Fact] + public void EnumTest() + { + verifier.Execute(code: """ +ConsoleApp.Run(args, (Fruit myFruit = Fruit.Apple, Fruit? moreFruit = null) => { }); + +enum Fruit +{ + Orange, Grape, Apple +} +""", + args: "--help", + expected: """ +Usage: [options...] [-h|--help] [--version] + +Options: + --my-fruit (Default: Apple) + --more-fruit (Default: null) + +"""); + } +} diff --git a/tests/ConsoleAppFramework.GeneratorTests/NameConverterTest.cs b/tests/ConsoleAppFramework.GeneratorTests/NameConverterTest.cs index 1702a66..b388f9f 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/NameConverterTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/NameConverterTest.cs @@ -33,7 +33,7 @@ public void KebabCase() public void CommmandName() { verifier.Execute(""" -var builder = ConsoleApp.CreateBuilder(); +var builder = ConsoleApp.Create(); builder.Add(); builder.Run(args); @@ -51,7 +51,7 @@ public void HelloWorld() public void OptionName() { verifier.Execute(""" -var builder = ConsoleApp.CreateBuilder(); +var builder = ConsoleApp.Create(); builder.Add(); builder.Run(args); @@ -66,7 +66,7 @@ public void HelloWorld(string fooBar) verifier.Execute(""" -var builder = ConsoleApp.CreateBuilder(); +var builder = ConsoleApp.Create(); var mc = new MyClass(); builder.Add("hello-world", mc.HelloWorld); builder.Run(args); diff --git a/tests/ConsoleAppFramework.GeneratorTests/SubCommandTest.cs b/tests/ConsoleAppFramework.GeneratorTests/SubCommandTest.cs index dbe3ef2..04fca04 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/SubCommandTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/SubCommandTest.cs @@ -15,7 +15,7 @@ public class SubCommandTest(ITestOutputHelper output) public void Zeroargs() { var code = """ -var builder = ConsoleApp.CreateBuilder(); +var builder = ConsoleApp.Create(); builder.Add("", () => { Console.Write("root"); }); builder.Add("a", () => { Console.Write("a"); }); @@ -43,7 +43,7 @@ public void Zeroargs() public void Withargs() { var code = """ -var builder = ConsoleApp.CreateBuilder(); +var builder = ConsoleApp.Create(); builder.Add("", (int x, int y) => { Console.Write($"root {x} {y}"); }); builder.Add("a", (int x, int y) => { Console.Write($"a {x} {y}"); }); @@ -71,7 +71,7 @@ public void Withargs() public void ZeroargsAsync() { var code = """ -var builder = ConsoleApp.CreateBuilder(); +var builder = ConsoleApp.Create(); builder.Add("", () => { Console.Write("root"); }); builder.Add("a", () => { Console.Write("a"); }); @@ -99,7 +99,7 @@ public void ZeroargsAsync() public void WithargsAsync() { var code = """ -var builder = ConsoleApp.CreateBuilder(); +var builder = ConsoleApp.Create(); builder.Add("", (int x, int y) => { Console.Write($"root {x} {y}"); }); builder.Add("a", (int x, int y) => { Console.Write($"a {x} {y}"); }); From f8b63cef4b273425cf25beb2339091e7491d9616 Mon Sep 17 00:00:00 2001 From: neuecc Date: Thu, 30 May 2024 21:26:38 +0900 Subject: [PATCH 38/54] done don done --- sandbox/CliFrameworkBenchmark/Benchmark.cs | 26 +++++++++++++++++++ .../Commands/ConsoleAppFrameworkCommand.cs | 24 +++++++++++++++++ .../GeneratorSandbox/GeneratorSandbox.csproj | 1 + sandbox/GeneratorSandbox/Program.cs | 15 +++++++++++ 4 files changed, 66 insertions(+) diff --git a/sandbox/CliFrameworkBenchmark/Benchmark.cs b/sandbox/CliFrameworkBenchmark/Benchmark.cs index be4769c..0a7fc0c 100644 --- a/sandbox/CliFrameworkBenchmark/Benchmark.cs +++ b/sandbox/CliFrameworkBenchmark/Benchmark.cs @@ -98,4 +98,30 @@ public void ExecuteSpectreConsoleCli() var app = new CommandApp(); app.Run(Arguments); } + + + //[Benchmark(Description = "ConsoleAppFramework Builder API")] + //public unsafe void ExecuteConsoleAppFramework2() + //{ + // var app = ConsoleApp.Create(); + // app.Add("", ConsoleAppFrameworkCommand.Execute); + // app.Run(Arguments); + //} + + //[Benchmark(Description = "ConsoleAppFramework CancellationToken")] + //public unsafe void ExecuteConsoleAppFramework3() + //{ + // var app = ConsoleApp.Create(); + // app.Add("", ConsoleAppFrameworkCommandWithCancellationToken.Execute); + // app.Run(Arguments); + //} + + //[Benchmark(Description = "ConsoleAppFramework With Filter")] + //public unsafe void ExecuteConsoleAppFramework4() + //{ + // var app = ConsoleApp.Create(); + // app.UseFilter(); + // app.Add("", ConsoleAppFrameworkCommand.Execute); + // app.Run(Arguments); + //} } \ No newline at end of file diff --git a/sandbox/CliFrameworkBenchmark/Commands/ConsoleAppFrameworkCommand.cs b/sandbox/CliFrameworkBenchmark/Commands/ConsoleAppFrameworkCommand.cs index 592ec67..c39613e 100644 --- a/sandbox/CliFrameworkBenchmark/Commands/ConsoleAppFrameworkCommand.cs +++ b/sandbox/CliFrameworkBenchmark/Commands/ConsoleAppFrameworkCommand.cs @@ -15,6 +15,8 @@ // } //} +using ConsoleAppFramework; + public class ConsoleAppFrameworkCommand { /// @@ -27,4 +29,26 @@ public static void Execute(string? str, int intOption, bool boolOption) { } +} + +public class ConsoleAppFrameworkCommandWithCancellationToken +{ + /// + /// + /// + /// -s + /// -i + /// -b + public static void Execute(string? str, int intOption, bool boolOption, CancellationToken cancellationToken) + { + + } +} + +internal class NopConsoleAppFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) +{ + public override Task InvokeAsync(CancellationToken cancellationToken) + { + return Next.InvokeAsync(cancellationToken); + } } \ No newline at end of file diff --git a/sandbox/GeneratorSandbox/GeneratorSandbox.csproj b/sandbox/GeneratorSandbox/GeneratorSandbox.csproj index 3287be0..4b548a3 100644 --- a/sandbox/GeneratorSandbox/GeneratorSandbox.csproj +++ b/sandbox/GeneratorSandbox/GeneratorSandbox.csproj @@ -11,6 +11,7 @@ + diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index 521de59..c80d744 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -17,9 +17,24 @@ using System.Threading.Tasks; using ConsoleAppFramework; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using static ConsoleAppFramework.ConsoleApp; +var host = Host.CreateApplicationBuilder().Build(); + + + + +host.Start(); // what is? + + +ConsoleApp.ServiceProvider = host.Services; + + + + + ConsoleApp.Run(args, (int xxx = 100, Fruit myFruit = Fruit.Apple) => { }); enum Fruit From 5f51c394cd22c11459278062646e1541b0910f31 Mon Sep 17 00:00:00 2001 From: neuecc Date: Fri, 31 May 2024 01:27:45 +0900 Subject: [PATCH 39/54] more test --- sandbox/GeneratorSandbox/Program.cs | 30 ++-- src/ConsoleAppFramework5/Command.cs | 3 +- .../ConsoleAppGenerator.cs | 11 +- .../DiagnosticDescriptors.cs | 16 ++ src/ConsoleAppFramework5/Parser.cs | 30 ++-- .../DiagnosticsTest.cs | 166 ++++++++++++++++++ .../HelpTest.cs | 35 ++++ .../RunTest.cs | 29 +++ 8 files changed, 287 insertions(+), 33 deletions(-) diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index c80d744..0872e49 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -21,28 +21,26 @@ using static ConsoleAppFramework.ConsoleApp; -var host = Host.CreateApplicationBuilder().Build(); - - - - -host.Start(); // what is? - - -ConsoleApp.ServiceProvider = host.Services; - - - - - -ConsoleApp.Run(args, (int xxx = 100, Fruit myFruit = Fruit.Apple) => { }); +ConsoleApp.Run(args, (int foo, string bar, Fruit ft, bool flag, Half half, int? itt, Obj obj) => +{ + Console.Write(foo); + Console.Write(bar); + Console.Write(ft); + Console.Write(flag); + Console.Write(half); + Console.Write(itt); + Console.Write(obj.Foo); +}); enum Fruit { Orange, Grape, Apple } - +public class Obj +{ + public int Foo { get; set; } +} diff --git a/src/ConsoleAppFramework5/Command.cs b/src/ConsoleAppFramework5/Command.cs index 6b0e9e1..dd76172 100644 --- a/src/ConsoleAppFramework5/Command.cs +++ b/src/ConsoleAppFramework5/Command.cs @@ -159,12 +159,13 @@ public record class CommandParameter public required bool IsFromServices { get; init; } public required bool IsCancellationToken { get; init; } public bool IsParsable => !(IsFromServices || IsCancellationToken); + public bool IsFlag => Type.SpecialType == SpecialType.System_Boolean; public required bool HasValidation { get; init; } public required int ArgumentIndex { get; init; } // -1 is not Argument, other than marked as [Argument] public bool IsArgument => ArgumentIndex != -1; public required string[] Aliases { get; init; } public required string Description { get; init; } - public bool RequireCheckArgumentParsed => !(HasDefaultValue || IsParams); + public bool RequireCheckArgumentParsed => !(HasDefaultValue || IsParams || IsFlag); public string BuildParseMethod(int argCount, string argumentName, WellKnownTypes wellKnownTypes, bool increment) { diff --git a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs index 760f057..4f78ab0 100644 --- a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework5/ConsoleAppGenerator.cs @@ -581,6 +581,7 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex }); var globalFilters = methodGroup["UseFilter"] + .OrderBy(x => x.Node.GetLocation().SourceSpan) // sort by line number .Select(x => { var genericName = (x.Node.Expression as MemberAccessExpressionSyntax)?.Name as GenericNameSyntax; @@ -592,14 +593,20 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex if (filter == null) { - // TODO: validation, ctor is invalid. + sourceProductionContext.ReportDiagnostic(DiagnosticDescriptors.FilterMultipleConsturtor, genericType.GetLocation()); + return null; } return filter!; }) - .Where(x => x != null) .ToArray(); + // don't emit if exists failure(already reported error) + if (globalFilters.Any(x => x == null)) + { + return; + } + var names = new HashSet(); var commands1 = methodGroup["Add"] .Select(x => diff --git a/src/ConsoleAppFramework5/DiagnosticDescriptors.cs b/src/ConsoleAppFramework5/DiagnosticDescriptors.cs index 7706038..632b821 100644 --- a/src/ConsoleAppFramework5/DiagnosticDescriptors.cs +++ b/src/ConsoleAppFramework5/DiagnosticDescriptors.cs @@ -66,4 +66,20 @@ public static DiagnosticDescriptor Create(int id, string title, string messageFo public static DiagnosticDescriptor CommandHasFilter { get; } = Create( 9, "ConsoleApp.Run does not allow the use of filters, but the function has a filter attribute."); + + public static DiagnosticDescriptor FilterMultipleConsturtor { get; } = Create( + 10, + "ConsoleAppFilter class does not allow multiple constructors."); + + public static DiagnosticDescriptor ClassMultipleConsturtor { get; } = Create( + 11, + "ConsoleAppBuilder.Add class does not allow multiple constructors."); + + public static DiagnosticDescriptor ClassHasNoPublicMethods { get; } = Create( + 12, + "ConsoleAppBuilder.Add class must have at least one public method."); + + public static DiagnosticDescriptor ClassIsStaticOrAbstract { get; } = Create( + 13, + "ConsoleAppBuilder.Add class does not allow static or abstract classes."); } diff --git a/src/ConsoleAppFramework5/Parser.cs b/src/ConsoleAppFramework5/Parser.cs index 2f3a782..8a98e1f 100644 --- a/src/ConsoleAppFramework5/Parser.cs +++ b/src/ConsoleAppFramework5/Parser.cs @@ -84,7 +84,8 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta if (type.IsStatic || type.IsAbstract) { - // TODO: validation + context.ReportDiagnostic(DiagnosticDescriptors.ClassIsStaticOrAbstract, node.GetLocation()); + return []; } var publicMethods = type.GetMembers() @@ -102,12 +103,14 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta if (publicMethods.Length == 0) { - // TODO: validation + context.ReportDiagnostic(DiagnosticDescriptors.ClassHasNoPublicMethods, node.GetLocation()); + return []; } if (publicConstructors.Length != 1) { - // TODO: validation + context.ReportDiagnostic(DiagnosticDescriptors.ClassMultipleConsturtor, node.GetLocation()); + return []; } var hasIDisposable = type.AllInterfaces.Any(x => SymbolEqualityComparer.Default.Equals(x, wellKnownTypes.IDisposable)); @@ -123,7 +126,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta if (filter == null) { - // TODO: validation, ctor is invalid. + context.ReportDiagnostic(DiagnosticDescriptors.FilterMultipleConsturtor, x.ApplicationSyntaxReference!.GetSyntax().GetLocation()); return null!; } @@ -131,8 +134,11 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta } return null!; }) - .Where(x => x != null) .ToArray(); + if (typeFilters.Any(x => x == null)) + { + return []; + } var methodInfoBase = new CommandMethodInfo { @@ -277,13 +283,6 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta } } - // bool is always optional flag - if (type.Type?.SpecialType == SpecialType.System_Boolean) - { - hasDefault = true; - defaultValue = false; - } - var hasParams = x.Modifiers.Any(x => x.IsKind(SyntaxKind.ParamsKeyword)); var customParserType = x.AttributeLists.SelectMany(x => x.Attributes) @@ -463,7 +462,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta if (filter == null) { - // TODO: validation, ctor is invalid. + context.ReportDiagnostic(DiagnosticDescriptors.FilterMultipleConsturtor, x.ApplicationSyntaxReference!.GetSyntax().GetLocation()); return null!; } @@ -471,8 +470,11 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta } return null!; }) - .Where(x => x != null) .ToArray(); + if (methodFilters.Any(x => x == null)) + { + return null; + } var parsableIndex = 0; var parameters = methodSymbol.Parameters diff --git a/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs b/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs index 18abb21..7d59683 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs @@ -268,4 +268,170 @@ public override Task InvokeAsync(CancellationToken cancellationToken) } """, "ConsoleApp.Run(args, Hello)"); } + + [Fact] + public void MultiConstructorFilter() + { + verifier.Verify(10, """ +var app = ConsoleApp.Create(); +app.UseFilter(); +app.Add("", Hello); +app.Run(args); + +void Hello() +{ +} + +internal class NopFilter : ConsoleAppFilter +{ + public NopFilter(ConsoleAppFilter next) + :base(next) + { + } + + public NopFilter(string x, ConsoleAppFilter next) + :base(next) + { + } + + public override Task InvokeAsync(CancellationToken cancellationToken) + { + return Next.InvokeAsync(cancellationToken); + } +} +""", "NopFilter"); + + verifier.Verify(10, """ +var app = ConsoleApp.Create(); +app.Add(); +app.Run(args); + +[ConsoleAppFilter] +public class Foo +{ + public void Hello() + { + } +} + +internal class NopFilter : ConsoleAppFilter +{ + public NopFilter(ConsoleAppFilter next) + :base(next) + { + } + + public NopFilter(string x, ConsoleAppFilter next) + :base(next) + { + } + + public override Task InvokeAsync(CancellationToken cancellationToken) + { + return Next.InvokeAsync(cancellationToken); + } +} +""", "ConsoleAppFilter"); + + verifier.Verify(10, """ +var app = ConsoleApp.Create(); +app.Add(); +app.Run(args); + +public class Foo +{ + [ConsoleAppFilter] + public void Hello() + { + } +} + +internal class NopFilter : ConsoleAppFilter +{ + public NopFilter(ConsoleAppFilter next) + :base(next) + { + } + + public NopFilter(string x, ConsoleAppFilter next) + :base(next) + { + } + + public override Task InvokeAsync(CancellationToken cancellationToken) + { + return Next.InvokeAsync(cancellationToken); + } +} +""", "ConsoleAppFilter"); + } + + + [Fact] + public void MultipleCtorClass() + { + verifier.Verify(11, """ +var app = ConsoleApp.Create(); +app.Add(); +app.Run(args); + +public class Foo +{ + public Foo() { } + public Foo(int x) { } + + public void Hello() + { + } +} +""", "app.Add()"); + } + + [Fact] + public void PublicMethods() + { + verifier.Verify(12, """ +var app = ConsoleApp.Create(); +app.Add(); +app.Run(args); + +public class Foo +{ + public Foo() { } + public Foo(int x) { } + + private void Hello() + { + } +} +""", "app.Add()"); + } + + [Fact] + public void AbstractNotAllow() + { + verifier.Verify(13, """ +var app = ConsoleApp.Create(); +app.Add(); +app.Run(args); + +public abstract class Foo +{ + public void Hello() + { + } +} +""", "app.Add()"); + + verifier.Verify(13, """ +var app = ConsoleApp.Create(); +app.Add(); +app.Run(args); + +public interface IFoo +{ + void Hello(); +} +""", "app.Add()"); + } } diff --git a/tests/ConsoleAppFramework.GeneratorTests/HelpTest.cs b/tests/ConsoleAppFramework.GeneratorTests/HelpTest.cs index a0f56ec..3b969c6 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/HelpTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/HelpTest.cs @@ -242,6 +242,41 @@ enum Fruit --my-fruit (Default: Apple) --more-fruit (Default: null) +"""); + } + + [Fact] + public void Summary2() + { + var code = """ +var app = ConsoleApp.Create(); +app.Add(); +app.Run(args); + +public class MyClass +{ + /// + /// hello my world. + /// + /// -b, my boo is not boo. + /// -f|-fb, my foo is not bar. + public void HelloWorld([Argument]int boo, string fooBar) + { + Console.Write("Hello World! " + fooBar); + } +} +"""; + verifier.Execute(code, args: "hello-world --help", expected: """ +Usage: hello-world [arguments...] [options...] [-h|--help] [--version] + +hello my world. + +Arguments: + [0] my boo is not boo. + +Options: + -f|-fb|--foo-bar my foo is not bar. (Required) + """); } } diff --git a/tests/ConsoleAppFramework.GeneratorTests/RunTest.cs b/tests/ConsoleAppFramework.GeneratorTests/RunTest.cs index c0d477e..53860ac 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/RunTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/RunTest.cs @@ -54,4 +54,33 @@ The field y must be between 100 and 200. Environment.ExitCode.Should().Be(1); Environment.ExitCode = 0; } + [Fact] + public void Parameters() + { + verifier.Execute(""" +ConsoleApp.Run(args, (int foo, string bar, Fruit ft, bool flag, Half half, int? itt, Takoyaki.Obj obj) => +{ + Console.Write(foo); + Console.Write(bar); + Console.Write(ft); + Console.Write(flag); + Console.Write(half); + Console.Write(itt); + Console.Write(obj.Foo); +}); + +enum Fruit +{ + Orange, Grape, Apple +} + +namespace Takoyaki +{ + public class Obj + { + public int Foo { get; set; } + } +} +""", "--foo 10 --bar aiueo --ft Grape --flag --half 1.3 --itt 99 --obj {\"Foo\":1999}", "10aiueoGrapeTrue1.3991999"); + } } \ No newline at end of file From 84cd0ed4e428cd7f2d4480e888a793b4d29c416c Mon Sep 17 00:00:00 2001 From: neuecc Date: Fri, 31 May 2024 01:40:46 +0900 Subject: [PATCH 40/54] delete v4 --- ConsoleAppFramework.sln | 53 +- .../CliFrameworkBenchmark.csproj | 2 +- .../GeneratorSandbox/GeneratorSandbox.csproj | 2 +- sandbox/GeneratorSandbox/Program.cs | 50 +- sandbox/MultiContainedApp/Dockerfile | 7 - .../MultiContainedApp.csproj | 14 - sandbox/MultiContainedApp/Program.cs | 131 ----- sandbox/Net6Console/GlobalUsing.cs | 1 - sandbox/Net6Console/MyDbContext.cs | 13 - sandbox/Net6Console/Net6Console.csproj | 23 - sandbox/Net6Console/Program.cs | 15 - sandbox/Net6WebApp/Net6WebApp.csproj | 11 - sandbox/Net6WebApp/Program.cs | 43 -- sandbox/SingleContainedApp/Program.cs | 254 --------- sandbox/SingleContainedApp/SampleFilter.cs | 51 -- .../SingleContainedApp.csproj | 24 - sandbox/SingleContainedApp/appsettings.json | 4 - .../SingleContainedAppWithConfig/Program.cs | 61 -- .../SingleContainedAppWithConfig.csproj | 36 -- .../appsettings.Development.json | 5 - .../appsettings.Production.json | 5 - .../appsettings.Staging.json | 5 - .../appsettings.json | 15 - .../.template.config/template.json | 11 - src/ConsoleAppFramework/AssemblyInfo.cs | 11 - .../Command.cs | 0 src/ConsoleAppFramework/CommandAttribute.cs | 37 -- src/ConsoleAppFramework/CommandDescriptor.cs | 84 --- .../CommandDescriptorCollection.cs | 184 ------ src/ConsoleAppFramework/CommandHelpBuilder.cs | 537 +++++++++--------- src/ConsoleAppFramework/ConsoleApp.cs | 255 --------- src/ConsoleAppFramework/ConsoleAppBase.cs | 11 - src/ConsoleAppFramework/ConsoleAppBuilder.cs | 223 -------- src/ConsoleAppFramework/ConsoleAppContext.cs | 44 -- src/ConsoleAppFramework/ConsoleAppEngine.cs | 518 ----------------- .../ConsoleAppEngineService.cs | 132 ----- src/ConsoleAppFramework/ConsoleAppFilter.cs | 122 ---- .../ConsoleAppFramework.csproj | 41 +- .../ConsoleAppFramework.props | 7 - .../ConsoleAppGenerator.cs | 2 +- src/ConsoleAppFramework/ConsoleAppOptions.cs | 77 --- src/ConsoleAppFramework/DefaultCommands.cs | 60 -- .../DiagnosticDescriptors.cs | 0 .../Emitter.cs | 0 src/ConsoleAppFramework/Icon.png | Bin 3185 -> 0 bytes .../LegacyCompatibleExtensions.cs | 86 --- .../NameConverter.cs | 0 src/ConsoleAppFramework/OptionAttribute.cs | 41 -- src/ConsoleAppFramework/ParamsValidator.cs | 75 --- .../Parser.cs | 42 +- .../Properties/launchSettings.json | 0 .../RoslynExtensions.cs | 0 .../SimpleConsoleLogger.cs | 114 ---- .../SourceBuilder.cs | 0 .../WellKnownTypes.cs | 0 src/ConsoleAppFramework/release.snk | Bin 596 -> 0 bytes .../CommandHelpBuilder.cs | 363 ------------ .../ConsoleAppFramework5.csproj | 26 - .../ConsoleAppFramework.GeneratorTests.csproj | 2 +- .../ConsoleAppFramework.Tests/AssemblyInfo.cs | 3 - .../ConsoleAppFramework.Tests.csproj | 27 - .../Integration/CaptureConsoleOutput.cs | 25 - .../Integration/CommandFilterTest.cs | 45 -- .../Integration/HelpUsageTest.cs | 37 -- .../Integration/InterceptorTest.cs | 105 ---- .../Integration/MultipleCommandTest.cs | 225 -------- .../Integration/NamedSingleCommandTest.cs | 83 --- .../SingleCommandTest.Arguments.cs | 154 ----- .../Integration/SingleCommandTest.Options.cs | 216 ------- .../SingleCommandTest.OptionsAndArguments.cs | 97 ---- .../Integration/SingleCommandTest.cs | 68 --- .../Integration/ValidationAttributeTests.cs | 65 --- .../Legacy/CommandAttributeTest.cs | 44 -- .../Legacy/CommandHelpTest.cs | 337 ----------- .../Legacy/ExitCodeTest.cs | 111 ---- .../Legacy/MultiContainedTest.cs | 81 --- .../Legacy/ParameterCheckTest.cs | 46 -- .../Legacy/SingleContainedTest.cs | 256 --------- .../Legacy/SubCommandTest.cs | 216 ------- .../ConsoleAppFramework.Tests/XUnitLogger.cs | 201 ------- 80 files changed, 304 insertions(+), 6068 deletions(-) delete mode 100644 sandbox/MultiContainedApp/Dockerfile delete mode 100644 sandbox/MultiContainedApp/MultiContainedApp.csproj delete mode 100644 sandbox/MultiContainedApp/Program.cs delete mode 100644 sandbox/Net6Console/GlobalUsing.cs delete mode 100644 sandbox/Net6Console/MyDbContext.cs delete mode 100644 sandbox/Net6Console/Net6Console.csproj delete mode 100644 sandbox/Net6Console/Program.cs delete mode 100644 sandbox/Net6WebApp/Net6WebApp.csproj delete mode 100644 sandbox/Net6WebApp/Program.cs delete mode 100644 sandbox/SingleContainedApp/Program.cs delete mode 100644 sandbox/SingleContainedApp/SampleFilter.cs delete mode 100644 sandbox/SingleContainedApp/SingleContainedApp.csproj delete mode 100644 sandbox/SingleContainedApp/appsettings.json delete mode 100644 sandbox/SingleContainedAppWithConfig/Program.cs delete mode 100644 sandbox/SingleContainedAppWithConfig/SingleContainedAppWithConfig.csproj delete mode 100644 sandbox/SingleContainedAppWithConfig/appsettings.Development.json delete mode 100644 sandbox/SingleContainedAppWithConfig/appsettings.Production.json delete mode 100644 sandbox/SingleContainedAppWithConfig/appsettings.Staging.json delete mode 100644 sandbox/SingleContainedAppWithConfig/appsettings.json delete mode 100644 src/ConsoleAppFramework/.template.config/template.json delete mode 100644 src/ConsoleAppFramework/AssemblyInfo.cs rename src/{ConsoleAppFramework5 => ConsoleAppFramework}/Command.cs (100%) delete mode 100644 src/ConsoleAppFramework/CommandAttribute.cs delete mode 100644 src/ConsoleAppFramework/CommandDescriptor.cs delete mode 100644 src/ConsoleAppFramework/CommandDescriptorCollection.cs delete mode 100644 src/ConsoleAppFramework/ConsoleApp.cs delete mode 100644 src/ConsoleAppFramework/ConsoleAppBase.cs delete mode 100644 src/ConsoleAppFramework/ConsoleAppBuilder.cs delete mode 100644 src/ConsoleAppFramework/ConsoleAppContext.cs delete mode 100644 src/ConsoleAppFramework/ConsoleAppEngine.cs delete mode 100644 src/ConsoleAppFramework/ConsoleAppEngineService.cs delete mode 100644 src/ConsoleAppFramework/ConsoleAppFilter.cs delete mode 100644 src/ConsoleAppFramework/ConsoleAppFramework.props rename src/{ConsoleAppFramework5 => ConsoleAppFramework}/ConsoleAppGenerator.cs (99%) delete mode 100644 src/ConsoleAppFramework/ConsoleAppOptions.cs delete mode 100644 src/ConsoleAppFramework/DefaultCommands.cs rename src/{ConsoleAppFramework5 => ConsoleAppFramework}/DiagnosticDescriptors.cs (100%) rename src/{ConsoleAppFramework5 => ConsoleAppFramework}/Emitter.cs (100%) delete mode 100644 src/ConsoleAppFramework/Icon.png delete mode 100644 src/ConsoleAppFramework/LegacyCompatibleExtensions.cs rename src/{ConsoleAppFramework5 => ConsoleAppFramework}/NameConverter.cs (100%) delete mode 100644 src/ConsoleAppFramework/OptionAttribute.cs delete mode 100644 src/ConsoleAppFramework/ParamsValidator.cs rename src/{ConsoleAppFramework5 => ConsoleAppFramework}/Parser.cs (95%) rename src/{ConsoleAppFramework5 => ConsoleAppFramework}/Properties/launchSettings.json (100%) rename src/{ConsoleAppFramework5 => ConsoleAppFramework}/RoslynExtensions.cs (100%) delete mode 100644 src/ConsoleAppFramework/SimpleConsoleLogger.cs rename src/{ConsoleAppFramework5 => ConsoleAppFramework}/SourceBuilder.cs (100%) rename src/{ConsoleAppFramework5 => ConsoleAppFramework}/WellKnownTypes.cs (100%) delete mode 100644 src/ConsoleAppFramework/release.snk delete mode 100644 src/ConsoleAppFramework5/CommandHelpBuilder.cs delete mode 100644 src/ConsoleAppFramework5/ConsoleAppFramework5.csproj delete mode 100644 tests/ConsoleAppFramework.Tests/AssemblyInfo.cs delete mode 100644 tests/ConsoleAppFramework.Tests/ConsoleAppFramework.Tests.csproj delete mode 100644 tests/ConsoleAppFramework.Tests/Integration/CaptureConsoleOutput.cs delete mode 100644 tests/ConsoleAppFramework.Tests/Integration/CommandFilterTest.cs delete mode 100644 tests/ConsoleAppFramework.Tests/Integration/HelpUsageTest.cs delete mode 100644 tests/ConsoleAppFramework.Tests/Integration/InterceptorTest.cs delete mode 100644 tests/ConsoleAppFramework.Tests/Integration/MultipleCommandTest.cs delete mode 100644 tests/ConsoleAppFramework.Tests/Integration/NamedSingleCommandTest.cs delete mode 100644 tests/ConsoleAppFramework.Tests/Integration/SingleCommandTest.Arguments.cs delete mode 100644 tests/ConsoleAppFramework.Tests/Integration/SingleCommandTest.Options.cs delete mode 100644 tests/ConsoleAppFramework.Tests/Integration/SingleCommandTest.OptionsAndArguments.cs delete mode 100644 tests/ConsoleAppFramework.Tests/Integration/SingleCommandTest.cs delete mode 100644 tests/ConsoleAppFramework.Tests/Integration/ValidationAttributeTests.cs delete mode 100644 tests/ConsoleAppFramework.Tests/Legacy/CommandAttributeTest.cs delete mode 100644 tests/ConsoleAppFramework.Tests/Legacy/CommandHelpTest.cs delete mode 100644 tests/ConsoleAppFramework.Tests/Legacy/ExitCodeTest.cs delete mode 100644 tests/ConsoleAppFramework.Tests/Legacy/MultiContainedTest.cs delete mode 100644 tests/ConsoleAppFramework.Tests/Legacy/ParameterCheckTest.cs delete mode 100644 tests/ConsoleAppFramework.Tests/Legacy/SingleContainedTest.cs delete mode 100644 tests/ConsoleAppFramework.Tests/Legacy/SubCommandTest.cs delete mode 100644 tests/ConsoleAppFramework.Tests/XUnitLogger.cs diff --git a/ConsoleAppFramework.sln b/ConsoleAppFramework.sln index a33e143..9ae6042 100644 --- a/ConsoleAppFramework.sln +++ b/ConsoleAppFramework.sln @@ -5,20 +5,10 @@ VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{1F399F98-7439-4F05-847B-CC1267B4B7F2}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleAppFramework", "src\ConsoleAppFramework\ConsoleAppFramework.csproj", "{AEBCDB61-F8FA-40EB-B6D9-636D112BC390}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sandbox", "sandbox", "{A2CF2984-E8E2-48FC-B5A1-58D74A2467E6}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SingleContainedApp", "sandbox\SingleContainedApp\SingleContainedApp.csproj", "{017F402E-36EA-46B3-A5AF-1773EEB7B755}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MultiContainedApp", "sandbox\MultiContainedApp\MultiContainedApp.csproj", "{5E16EBD8-3396-4952-9B2D-DD2E2E3C883B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SingleContainedAppWithConfig", "sandbox\SingleContainedAppWithConfig\SingleContainedAppWithConfig.csproj", "{0B3BE82E-753A-415D-AD4E-90350C6E5C3D}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{AAD2D900-C305-4449-A9FC-6C7696FFEDFA}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleAppFramework.Tests", "tests\ConsoleAppFramework.Tests\ConsoleAppFramework.Tests.csproj", "{AF15C841-5D45-4E61-BFCE-A6E6B7BA7629}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6DF6534A-0F9D-44A4-BF89-AE1F3B243914}" ProjectSection(SolutionItems) = preProject .dockerignore = .dockerignore @@ -28,17 +18,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ReadMe.md = ReadMe.md EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Net6WebApp", "sandbox\Net6WebApp\Net6WebApp.csproj", "{48781D9F-D3E0-4A72-ADD1-A47ECDC23F0A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Net6Console", "sandbox\Net6Console\Net6Console.csproj", "{19E33348-979A-4283-A74D-0844CC384A88}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleAppFramework5", "src\ConsoleAppFramework5\ConsoleAppFramework5.csproj", "{09BEEA7B-B6D3-4011-BCAB-6DF976713695}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleAppFramework", "src\ConsoleAppFramework\ConsoleAppFramework.csproj", "{09BEEA7B-B6D3-4011-BCAB-6DF976713695}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GeneratorSandbox", "sandbox\GeneratorSandbox\GeneratorSandbox.csproj", "{ACDA48BA-0BFE-4917-B335-7836DAA5929A}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFrameworkBenchmark", "sandbox\CliFrameworkBenchmark\CliFrameworkBenchmark.csproj", "{F558E4F2-1AB0-4634-B613-69DFE79894AF}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleAppFramework.GeneratorTests", "tests\ConsoleAppFramework.GeneratorTests\ConsoleAppFramework.GeneratorTests.csproj", "{C54F7FE8-650A-4DC7-877F-0DE929351800}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleAppFramework.GeneratorTests", "tests\ConsoleAppFramework.GeneratorTests\ConsoleAppFramework.GeneratorTests.csproj", "{C54F7FE8-650A-4DC7-877F-0DE929351800}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -46,34 +32,6 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {AEBCDB61-F8FA-40EB-B6D9-636D112BC390}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AEBCDB61-F8FA-40EB-B6D9-636D112BC390}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AEBCDB61-F8FA-40EB-B6D9-636D112BC390}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AEBCDB61-F8FA-40EB-B6D9-636D112BC390}.Release|Any CPU.Build.0 = Release|Any CPU - {017F402E-36EA-46B3-A5AF-1773EEB7B755}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {017F402E-36EA-46B3-A5AF-1773EEB7B755}.Debug|Any CPU.Build.0 = Debug|Any CPU - {017F402E-36EA-46B3-A5AF-1773EEB7B755}.Release|Any CPU.ActiveCfg = Release|Any CPU - {017F402E-36EA-46B3-A5AF-1773EEB7B755}.Release|Any CPU.Build.0 = Release|Any CPU - {5E16EBD8-3396-4952-9B2D-DD2E2E3C883B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5E16EBD8-3396-4952-9B2D-DD2E2E3C883B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5E16EBD8-3396-4952-9B2D-DD2E2E3C883B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5E16EBD8-3396-4952-9B2D-DD2E2E3C883B}.Release|Any CPU.Build.0 = Release|Any CPU - {0B3BE82E-753A-415D-AD4E-90350C6E5C3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0B3BE82E-753A-415D-AD4E-90350C6E5C3D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0B3BE82E-753A-415D-AD4E-90350C6E5C3D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0B3BE82E-753A-415D-AD4E-90350C6E5C3D}.Release|Any CPU.Build.0 = Release|Any CPU - {AF15C841-5D45-4E61-BFCE-A6E6B7BA7629}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AF15C841-5D45-4E61-BFCE-A6E6B7BA7629}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AF15C841-5D45-4E61-BFCE-A6E6B7BA7629}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AF15C841-5D45-4E61-BFCE-A6E6B7BA7629}.Release|Any CPU.Build.0 = Release|Any CPU - {48781D9F-D3E0-4A72-ADD1-A47ECDC23F0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {48781D9F-D3E0-4A72-ADD1-A47ECDC23F0A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {48781D9F-D3E0-4A72-ADD1-A47ECDC23F0A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {48781D9F-D3E0-4A72-ADD1-A47ECDC23F0A}.Release|Any CPU.Build.0 = Release|Any CPU - {19E33348-979A-4283-A74D-0844CC384A88}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {19E33348-979A-4283-A74D-0844CC384A88}.Debug|Any CPU.Build.0 = Debug|Any CPU - {19E33348-979A-4283-A74D-0844CC384A88}.Release|Any CPU.ActiveCfg = Release|Any CPU - {19E33348-979A-4283-A74D-0844CC384A88}.Release|Any CPU.Build.0 = Release|Any CPU {09BEEA7B-B6D3-4011-BCAB-6DF976713695}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {09BEEA7B-B6D3-4011-BCAB-6DF976713695}.Debug|Any CPU.Build.0 = Debug|Any CPU {09BEEA7B-B6D3-4011-BCAB-6DF976713695}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -95,13 +53,6 @@ Global HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {AEBCDB61-F8FA-40EB-B6D9-636D112BC390} = {1F399F98-7439-4F05-847B-CC1267B4B7F2} - {017F402E-36EA-46B3-A5AF-1773EEB7B755} = {A2CF2984-E8E2-48FC-B5A1-58D74A2467E6} - {5E16EBD8-3396-4952-9B2D-DD2E2E3C883B} = {A2CF2984-E8E2-48FC-B5A1-58D74A2467E6} - {0B3BE82E-753A-415D-AD4E-90350C6E5C3D} = {A2CF2984-E8E2-48FC-B5A1-58D74A2467E6} - {AF15C841-5D45-4E61-BFCE-A6E6B7BA7629} = {AAD2D900-C305-4449-A9FC-6C7696FFEDFA} - {48781D9F-D3E0-4A72-ADD1-A47ECDC23F0A} = {A2CF2984-E8E2-48FC-B5A1-58D74A2467E6} - {19E33348-979A-4283-A74D-0844CC384A88} = {A2CF2984-E8E2-48FC-B5A1-58D74A2467E6} {09BEEA7B-B6D3-4011-BCAB-6DF976713695} = {1F399F98-7439-4F05-847B-CC1267B4B7F2} {ACDA48BA-0BFE-4917-B335-7836DAA5929A} = {A2CF2984-E8E2-48FC-B5A1-58D74A2467E6} {F558E4F2-1AB0-4634-B613-69DFE79894AF} = {A2CF2984-E8E2-48FC-B5A1-58D74A2467E6} diff --git a/sandbox/CliFrameworkBenchmark/CliFrameworkBenchmark.csproj b/sandbox/CliFrameworkBenchmark/CliFrameworkBenchmark.csproj index 2294410..359b7ae 100644 --- a/sandbox/CliFrameworkBenchmark/CliFrameworkBenchmark.csproj +++ b/sandbox/CliFrameworkBenchmark/CliFrameworkBenchmark.csproj @@ -24,7 +24,7 @@ - + Analyzer false diff --git a/sandbox/GeneratorSandbox/GeneratorSandbox.csproj b/sandbox/GeneratorSandbox/GeneratorSandbox.csproj index 4b548a3..cb5b690 100644 --- a/sandbox/GeneratorSandbox/GeneratorSandbox.csproj +++ b/sandbox/GeneratorSandbox/GeneratorSandbox.csproj @@ -15,7 +15,7 @@ - + Analyzer false diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index 0872e49..c31d8aa 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -20,39 +20,16 @@ using Microsoft.Extensions.Hosting; using static ConsoleAppFramework.ConsoleApp; +var builder = ConsoleApp.Create(); +builder.Add(); +builder.Run(args); -ConsoleApp.Run(args, (int foo, string bar, Fruit ft, bool flag, Half half, int? itt, Obj obj) => +public class MyClass() { - Console.Write(foo); - Console.Write(bar); - Console.Write(ft); - Console.Write(flag); - Console.Write(half); - Console.Write(itt); - Console.Write(obj.Foo); -}); - -enum Fruit -{ - Orange, Grape, Apple -} - -public class Obj -{ - public int Foo { get; set; } -} - - - -public class MyClass333 -{ - /// - /// -f|-fb, hello my world. - /// - /// - public void HelloWorld(string fooBar) + [Command("nomunomu")] + public void Do() { - Console.Write("Hello World! " + fooBar); + Console.Write("yeah"); } } @@ -76,8 +53,6 @@ public void HelloWorld(string fooBar) - - @@ -582,15 +557,6 @@ public void Do() } } - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] - internal sealed class CommandAttribute : Attribute - { - public string Command { get; } - - public CommandAttribute(string command) - { - this.Command = command; - } - } + } } \ No newline at end of file diff --git a/sandbox/MultiContainedApp/Dockerfile b/sandbox/MultiContainedApp/Dockerfile deleted file mode 100644 index a43210c..0000000 --- a/sandbox/MultiContainedApp/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM mcr.microsoft.com/dotnet/core/sdk:2.1 AS sdk -COPY . . -RUN dotnet publish /sandbox/MultiContainedApp/MultiContainedApp.csproj -c Release -o /app - -FROM mcr.microsoft.com/dotnet/core/runtime:2.1 -COPY --from=sdk /app . -ENTRYPOINT ["dotnet", "MultiContainedApp.dll"] \ No newline at end of file diff --git a/sandbox/MultiContainedApp/MultiContainedApp.csproj b/sandbox/MultiContainedApp/MultiContainedApp.csproj deleted file mode 100644 index 2c0d0a8..0000000 --- a/sandbox/MultiContainedApp/MultiContainedApp.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - Exe - net8.0 - 7.3 - false - - - - - - - diff --git a/sandbox/MultiContainedApp/Program.cs b/sandbox/MultiContainedApp/Program.cs deleted file mode 100644 index e90c42b..0000000 --- a/sandbox/MultiContainedApp/Program.cs +++ /dev/null @@ -1,131 +0,0 @@ -using ConsoleAppFramework; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using System; -using System.Diagnostics; -using System.Threading.Tasks; - -namespace MultiContainedApp -{ - class Program - { - static async Task Main(string[] args) - { - //d - //args = new string[] { "bar", "hello3", "-help" }; - //args = new string[] { "foo", "echo", "help"}; - //args = new string[] { "bar.hello2", "help" }; - args = new string[] { "foo-bar", "ec", "-msg", "tako" }; - - - await Host.CreateDefaultBuilder() - .ConfigureLogging(x => x.SetMinimumLevel(LogLevel.Trace)) - .RunConsoleAppFrameworkAsync(args, options: new ConsoleAppOptions - { - GlobalFilters = new ConsoleAppFilter[] { new MyFilter2 { Order = -1 }, new MyFilter() } - }); - - - - //await Host.CreateDefaultBuilder() - // .ConfigureLogging(x => - // { - // x.ClearProviders(); - // x.SetMinimumLevel(LogLevel.Trace); - - // }) - // .RunConsoleAppFrameworkAsync(args); - } - } - - [ConsoleAppFilter(typeof(MyFilter2), Order = 9999)] - [ConsoleAppFilter(typeof(MyFilter2), Order = 9999)] - // [Command("AAA")] - public class FooBar : ConsoleAppBase - { - [Command("ec", "My echo")] - public void Echo(string msg) - { - Console.WriteLine(msg + "OK??"); - } - - public void Sum(int x, int y) - { - Console.WriteLine((x + y).ToString()); - } - } - - public class Bar : ConsoleAppBase - { - [Command("ec", "My echo")] - public void Hello2(string msg) - { - Console.WriteLine("H E L L O 2"); - } - - - public void Sum(int x, int y) - { - Console.WriteLine((x + y).ToString()); - } - } - - - - //public class Foo : ConsoleAppBase - //{ - // [Command(new[] { "eo", "t" }, "Echo message to the logger")] - // [ConsoleAppFilter(typeof(EchoFilter), Order = 10000)] - // public void Echo([Option("msg", "Message to send.")]string msg) - // { - // // Console.WriteLine(new StackTrace().ToString()); - // this.Context.Logger.LogInformation(msg); - // } - - // [Command("s")] - // public void Sum([Option(0)]int x, [Option(1)]int y) - // { - // this.Context.Logger.LogInformation((x + y).ToString()); - // } - //} - - //public class Bar : ConsoleAppBase - //{ - // public void Hello2() - // { - // this.Context.Logger.LogInformation("H E L L O"); - // } - - // public void Hello3([Option(0)]int aaa) - // { - // this.Context.Logger.LogInformation("H E L L O:" + aaa); - // } - //} - - public class MyFilter : ConsoleAppFilter - { - public async override ValueTask Invoke(ConsoleAppContext context, Func next) - { - Console.WriteLine("call second"); - await next(context); - } - } - - public class MyFilter2 : ConsoleAppFilter - { - public async override ValueTask Invoke(ConsoleAppContext context, Func next) - { - Console.WriteLine("call first"); - await next(context); - } - } - - public class EchoFilter : ConsoleAppFilter - { - public async override ValueTask Invoke(ConsoleAppContext context, Func next) - { - Console.WriteLine("call ec"); - await next(context); - } - } -} diff --git a/sandbox/Net6Console/GlobalUsing.cs b/sandbox/Net6Console/GlobalUsing.cs deleted file mode 100644 index 1638fa4..0000000 --- a/sandbox/Net6Console/GlobalUsing.cs +++ /dev/null @@ -1 +0,0 @@ -global using ConsoleAppFramework; \ No newline at end of file diff --git a/sandbox/Net6Console/MyDbContext.cs b/sandbox/Net6Console/MyDbContext.cs deleted file mode 100644 index 64aaa49..0000000 --- a/sandbox/Net6Console/MyDbContext.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Net6Console -{ - public class MyDbContext : DbContext - { - } -} diff --git a/sandbox/Net6Console/Net6Console.csproj b/sandbox/Net6Console/Net6Console.csproj deleted file mode 100644 index 8adc9de..0000000 --- a/sandbox/Net6Console/Net6Console.csproj +++ /dev/null @@ -1,23 +0,0 @@ - - - - Exe - net6.0 - enable - enable - false - - - - - - - - - - - - - - - diff --git a/sandbox/Net6Console/Program.cs b/sandbox/Net6Console/Program.cs deleted file mode 100644 index 1773ea4..0000000 --- a/sandbox/Net6Console/Program.cs +++ /dev/null @@ -1,15 +0,0 @@ -using ConsoleAppFramework; -using System.Runtime.InteropServices; - - -// RegisterShutdownHandlers(); - -//var r2 = Console.ReadKey(); - - -// Console.ReadKey -ConsoleApp.Run(args, async (ConsoleAppContext ctx) => -{ - var key = await Task.Run(() => Console.ReadKey(intercept: true)).WaitAsync(ctx.CancellationToken); - Console.WriteLine(key.KeyChar); -}); diff --git a/sandbox/Net6WebApp/Net6WebApp.csproj b/sandbox/Net6WebApp/Net6WebApp.csproj deleted file mode 100644 index 5fe49b0..0000000 --- a/sandbox/Net6WebApp/Net6WebApp.csproj +++ /dev/null @@ -1,11 +0,0 @@ - - - - Exe - net6.0 - enable - enable - false - - - diff --git a/sandbox/Net6WebApp/Program.cs b/sandbox/Net6WebApp/Program.cs deleted file mode 100644 index 297c40d..0000000 --- a/sandbox/Net6WebApp/Program.cs +++ /dev/null @@ -1,43 +0,0 @@ - - - -var sc = new ServiceCollection(); -sc.AddTransient(); - -#pragma warning disable ASP0000 // Do not call 'IServiceCollection.BuildServiceProvider' in 'ConfigureServices' -var p = sc.BuildServiceProvider(); -#pragma warning restore ASP0000 // Do not call 'IServiceCollection.BuildServiceProvider' in 'ConfigureServices' - -var b = WebApplication.CreateBuilder(); - - - -b.Logging.AddConsole(); - -//.Services.loggi - - -//var builder = WebApplication.CreateBuilder(args); -//builder.Logging - - -var hoge = ActivatorUtilities.CreateInstance(p, typeof(MyClass2), new object[] { 1, 2, "hoge", new MyClass3() }); - - - -public class MyClass -{ - -} - -public class MyClass2 -{ - public MyClass2(int x, MyClass3 mc3, MyClass mc, string z, int y) - { - Console.WriteLine("OK:" + (x, mc, y, mc3, z)); - } -} - -public class MyClass3 -{ -} \ No newline at end of file diff --git a/sandbox/SingleContainedApp/Program.cs b/sandbox/SingleContainedApp/Program.cs deleted file mode 100644 index 3a097f7..0000000 --- a/sandbox/SingleContainedApp/Program.cs +++ /dev/null @@ -1,254 +0,0 @@ -#pragma warning disable CS1998 - -using ConsoleAppFramework; -using ConsoleAppFramework.Logging; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using System; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace SingleContainedApp -{ - public class MyFirstBatch : ConsoleAppBase - { - public void Hello( - [Option("n", "name of send user.")] string name, - [Option("r", "repeat count.")] int repeat = 3) - { - for (int i = 0; i < repeat; i++) - { - this.Context.Logger.LogInformation($"Hello My Batch from {name}"); - } - } - - IOptions config; - ILogger logger; - - public MyFirstBatch(IOptions config, ILogger logger) - { - this.config = config; - this.logger = logger; - } - - [Command("log")] - public void LogWrite() - { - Context.Logger.LogTrace("t r a c e"); - Context.Logger.LogDebug("d e b u g"); - Context.Logger.LogInformation("i n f o"); - Context.Logger.LogCritical("c r i t i c a l"); - Context.Logger.LogWarning("w a r n"); - Context.Logger.LogError("e r r o r"); - } - - [Command("opt")] - public void ShowOption() - { - Console.WriteLine(config.Value.Bar); - Console.WriteLine(config.Value.Foo); - } - - - [Command("version", "yeah, go!")] - public void ShowVersion() - { - var version = Assembly.GetExecutingAssembly() - .GetCustomAttribute() - .Version; - Console.WriteLine(version); - } - - [Command("escape")] - public void UrlEscape([Option(0)] string input) - { - Console.WriteLine(Uri.EscapeDataString(input)); - } - - [Command("timer")] - public async Task Timer([Option(0)] uint waitSeconds) - { - Console.WriteLine(waitSeconds + " seconds"); - while (waitSeconds != 0) - { - await Task.Delay(TimeSpan.FromSeconds(1), Context.CancellationToken); - waitSeconds--; - Console.WriteLine(waitSeconds + " seconds"); - } - } - } - - public class MyConfig - { - public int Foo { get; set; } - public bool Bar { get; set; } - } - - public class OverrideCheck : ConsoleAppBase - { - [Command("encode", "encode input string to base64url")] - public void Encode([Option(0)] string input) => Console.WriteLine((input)); - - [Command("decode", "decode input base64url to string")] - public void Decode([Option(0)] string input) => Console.WriteLine((input)); - - [Command("escape", "escape base64 to base64url")] - public void Escape([Option(0)] string input) => Console.WriteLine((input)); - - [Command(new[] { "unescape", "-h" }, "unescape base64url to base64")] - public void Unescape([Option(0)] string input) => Console.WriteLine((input)); - - //[Command(new[] { "help", "-h", "-help", "--help" }, "show help")] - //public void Help() - //{ - // Console.WriteLine("Usage: base64urls [-version] [-help] [decode|encode|escape|unescape] [args]"); - // Console.WriteLine("E.g., run this: base64urls decode QyMgaXMgYXdlc29tZQ=="); - // Console.WriteLine("E.g., run this: base64urls encode \"C# is awesome.\""); - // Console.WriteLine("E.g., run this: base64urls escape \"This+is/goingto+escape==\""); - // Console.WriteLine("E.g., run this: base64urls unescape \"This-is_goingto-escape\""); - //} - } - - public class ComplexArgTest : ConsoleAppBase - { - public void Foo(int[] array, Person person) - { - Context.Logger.LogTrace(array.Length + ":" + string.Join(", ", array)); - Context.Logger.LogInformation(person.Age + ":" + person.Name); - } - } - - public class StandardArgTest : ConsoleAppBase - { - public void Run([Option(0, "message of x.")] string x) - { - // Console.WriteLine("1." + x); - //Console.WriteLine("2." + y); - } - } - - public class Person - { - public int Age { get; set; } - public string Name { get; set; } - } - - - public class ThrowOperationCanceledException : ConsoleAppBase - { - public async Task Throw() - { - //while (true) - //{ - // await Task.Delay(10); - // Context.CancellationToken.ThrowIfCancellationRequested(); - //} - - var cts = new CancellationTokenSource(); - cts.Cancel(); - cts.Token.ThrowIfCancellationRequested(); - } - } - - - public class SimpleTwoArgs : ConsoleAppBase - { - public async ValueTask Hello([Option("n")] string name, [Option("r")] int repeat) - { - Context.Logger.LogInformation($"name:{name}"); - - Context.Logger.LogInformation($"Wait {repeat} Seconds."); - await Task.Delay(TimeSpan.FromSeconds(repeat)); - - Context.Logger.LogInformation($"repeat:{repeat}"); - - return 100; - } - } - - public class Issue46 : ConsoleAppBase, IDisposable - { - - public void Run(string str, bool b = false) - { - Console.WriteLine("str:" + str + " b:" + b); - } - - void IDisposable.Dispose() - { - Console.WriteLine("DISPOSE!"); - } - } - - //class Program - //{ - // static async Task Main(string[] args) - // { - // //args = new[] { "-array", "10,20,30", "-person", @"{""Age"":10,""Name"":""foo""}" }; - - // //args = new[] { "--name", "aaa", "--repeat", "3" }; - - // //args = new[] { "--help" }; - // //args = new[] { "encode", "--help" }; - // //args = new[] { "-str", "input" }; - // //args = new[] { "-str", "input", "-b"}; - // //args = new[] { "-str" }; - - // await Host.CreateDefaultBuilder() - // .ConfigureLogging(logging => - // { - // logging.SetMinimumLevel(LogLevel.Trace).ReplaceToSimpleConsole(); - // }) - // .RunConsoleAppFrameworkAsync(args); - // // .RunConsoleAppEngineAsync - // //.ConfigureServices((hostContext, services) => - // //{ - // // // mapping config json to IOption - // // services.Configure(hostContext.Configuration); - // //}) - // //.RunConsoleAppEngineAsync(args); - // } - //} - - public class Program : ConsoleAppBase, IDisposable, IAsyncDisposable - { - static async Task Main(string[] args) - { - //args = new[] { "-m", "a ", "-b", "False" }; - args = new[] { "hello" }; - - await Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args, new ConsoleAppOptions - { - //StrictOption = true, // default is false. - //ShowDefaultCommand = false, // default is true - }); - - } - - - public void Hello( - int? foo = null, - [Option("", "", DefaultValue = "DateTime.Today")] DateTime? hello = null) - { - if (hello == null) hello = DateTime.Now; - Console.WriteLine(hello); - } - - void IDisposable.Dispose() - { - Console.WriteLine("standard dispose"); - } - - async ValueTask IAsyncDisposable.DisposeAsync() - { - Console.WriteLine("async dispose"); - } - } -} diff --git a/sandbox/SingleContainedApp/SampleFilter.cs b/sandbox/SingleContainedApp/SampleFilter.cs deleted file mode 100644 index 3004292..0000000 --- a/sandbox/SingleContainedApp/SampleFilter.cs +++ /dev/null @@ -1,51 +0,0 @@ -using ConsoleAppFramework; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace SingleContainedApp -{ - public class LogRunningTimeFilter : ConsoleAppFilter - { - public override async ValueTask Invoke(ConsoleAppContext context, Func next) - { - context.Logger.LogInformation("Call method at " + context.Timestamp.ToLocalTime()); // LocalTime for human readable time - try - { - await next(context); - context.Logger.LogInformation("Call method Completed successfully, Elapsed:" + (DateTimeOffset.UtcNow - context.Timestamp)); - } - catch - { - context.Logger.LogInformation("Call method Completed Failed, Elapsed:" + (DateTimeOffset.UtcNow - context.Timestamp)); - throw; - } - } - } - - public class MutexFilter : ConsoleAppFilter - { - public override async ValueTask Invoke(ConsoleAppContext context, Func next) - { - using (var mutex = new Mutex(false, context.MethodInfo.Name)) - { - if (!mutex.WaitOne(0, false)) - { - throw new Exception($"already running {context.MethodInfo.Name} in another process."); - } - - try - { - await next(context); - } - finally - { - mutex.ReleaseMutex(); - } - } - } - } -} diff --git a/sandbox/SingleContainedApp/SingleContainedApp.csproj b/sandbox/SingleContainedApp/SingleContainedApp.csproj deleted file mode 100644 index ddca127..0000000 --- a/sandbox/SingleContainedApp/SingleContainedApp.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - Exe - net8.0 - 7.3 - false - - - - - - - - - Always - - - - - - - - diff --git a/sandbox/SingleContainedApp/appsettings.json b/sandbox/SingleContainedApp/appsettings.json deleted file mode 100644 index d3ec32f..0000000 --- a/sandbox/SingleContainedApp/appsettings.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "Foo": 42, - "Bar": true -} \ No newline at end of file diff --git a/sandbox/SingleContainedAppWithConfig/Program.cs b/sandbox/SingleContainedAppWithConfig/Program.cs deleted file mode 100644 index 9f5400a..0000000 --- a/sandbox/SingleContainedAppWithConfig/Program.cs +++ /dev/null @@ -1,61 +0,0 @@ -using ConsoleAppFramework; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using System.Threading.Tasks; - -public class Baz : ConsoleAppBase -{ - private readonly IOptions config; - // Batche inject Config on constructor. - public Baz(IOptions config, MyServiceA serviceA, MyServiceB serviceB, MyServiceC serviceC) - { - this.config = config; - } - - public void Hello3() - { - // https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.loglevel?view=aspnetcore-2.2 - this.Context.Logger.LogTrace("Trace"); // 0 - this.Context.Logger.LogDebug("Debug"); // 1 - this.Context.Logger.LogInformation("Info"); // 2 - this.Context.Logger.LogWarning("Warning"); // 3 - this.Context.Logger.LogError("Error"); // 4 - this.Context.Logger.LogInformation($"GlobalValue: {config.Value.GlobalValue}, EnvValue: {config.Value.EnvValue}"); - } -} - -public class MyServiceA { } -public class MyServiceB { } -public class MyServiceC { } - -namespace SingleContainedAppWithConfig -{ - class Program - { - static async Task Main(string[] args) - { - // using ConsoleAppFramework.Configuration; - await Host.CreateDefaultBuilder() - .ConfigureServices((hostContext, services) => - { - services.AddOptions(); - // mapping json element to class - services.Configure(hostContext.Configuration.GetSection("AppConfig")); - - services.AddScoped(); - services.AddTransient(); - services.AddSingleton(); - }) - .RunConsoleAppFrameworkAsync(args); - } - } - - // config mapping class - public class AppConfig - { - public string GlobalValue { get; set; } - public string EnvValue { get; set; } - } -} \ No newline at end of file diff --git a/sandbox/SingleContainedAppWithConfig/SingleContainedAppWithConfig.csproj b/sandbox/SingleContainedAppWithConfig/SingleContainedAppWithConfig.csproj deleted file mode 100644 index e863f1c..0000000 --- a/sandbox/SingleContainedAppWithConfig/SingleContainedAppWithConfig.csproj +++ /dev/null @@ -1,36 +0,0 @@ - - - - Exe - net8.0 - 7.3 - false - - - - - - - - - - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - - - - - - diff --git a/sandbox/SingleContainedAppWithConfig/appsettings.Development.json b/sandbox/SingleContainedAppWithConfig/appsettings.Development.json deleted file mode 100644 index 83f4c0d..0000000 --- a/sandbox/SingleContainedAppWithConfig/appsettings.Development.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "AppConfig": { - "EnvValue": "ENV VALUE!!!!(DEVELOPMENT)" - } -} diff --git a/sandbox/SingleContainedAppWithConfig/appsettings.Production.json b/sandbox/SingleContainedAppWithConfig/appsettings.Production.json deleted file mode 100644 index 14c3732..0000000 --- a/sandbox/SingleContainedAppWithConfig/appsettings.Production.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "AppConfig": { - "EnvValue": "ENV VALUE!!!!(PRODUCTION)" - } -} diff --git a/sandbox/SingleContainedAppWithConfig/appsettings.Staging.json b/sandbox/SingleContainedAppWithConfig/appsettings.Staging.json deleted file mode 100644 index c113821..0000000 --- a/sandbox/SingleContainedAppWithConfig/appsettings.Staging.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "AppConfig": { - "EnvValue": "ENV VALUE!!!!(STAGING)" - } -} diff --git a/sandbox/SingleContainedAppWithConfig/appsettings.json b/sandbox/SingleContainedAppWithConfig/appsettings.json deleted file mode 100644 index bf7b454..0000000 --- a/sandbox/SingleContainedAppWithConfig/appsettings.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "AppConfig": { - "GlobalValue": "GLOBAL VALUE!!!!", - "EnvValue": "ENV VALUE!!!!" - }, - "Logging": { - "ConsoleAppFramework.Logging.SimpleConsoleLoggerProvider": { - "LogLevel": { - "Default": "Trace", - "System": "Information", - "Microsoft": "Information" - } - } - } -} diff --git a/src/ConsoleAppFramework/.template.config/template.json b/src/ConsoleAppFramework/.template.config/template.json deleted file mode 100644 index 4f4f2c8..0000000 --- a/src/ConsoleAppFramework/.template.config/template.json +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Text; - -[assembly:InternalsVisibleTo("ConsoleAppFramework.Tests, PublicKey="+ - "002400000480000094000000060200000024000052534131000400000100010089b12412c27f5f"+ - "aece3868b659bf311f2550c5cc1b4cbbe032a048ec36fa288872d51e8ddfd77a83e835c6ea3940"+ - "c331fe89d1ba9146a12abed588f194cd437cfe81252634d49214acf6b11dc9e97cfc6d0f818082"+ - "e5bbafb50e890eed19517c199213075b294d5fa59556cd42186041a1a95f8cff3869575bed4de2"+ - "dd8f5dc2")] \ No newline at end of file diff --git a/src/ConsoleAppFramework/AssemblyInfo.cs b/src/ConsoleAppFramework/AssemblyInfo.cs deleted file mode 100644 index 4f4f2c8..0000000 --- a/src/ConsoleAppFramework/AssemblyInfo.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Text; - -[assembly:InternalsVisibleTo("ConsoleAppFramework.Tests, PublicKey="+ - "002400000480000094000000060200000024000052534131000400000100010089b12412c27f5f"+ - "aece3868b659bf311f2550c5cc1b4cbbe032a048ec36fa288872d51e8ddfd77a83e835c6ea3940"+ - "c331fe89d1ba9146a12abed588f194cd437cfe81252634d49214acf6b11dc9e97cfc6d0f818082"+ - "e5bbafb50e890eed19517c199213075b294d5fa59556cd42186041a1a95f8cff3869575bed4de2"+ - "dd8f5dc2")] \ No newline at end of file diff --git a/src/ConsoleAppFramework5/Command.cs b/src/ConsoleAppFramework/Command.cs similarity index 100% rename from src/ConsoleAppFramework5/Command.cs rename to src/ConsoleAppFramework/Command.cs diff --git a/src/ConsoleAppFramework/CommandAttribute.cs b/src/ConsoleAppFramework/CommandAttribute.cs deleted file mode 100644 index 986c0c0..0000000 --- a/src/ConsoleAppFramework/CommandAttribute.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; - -namespace ConsoleAppFramework -{ - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)] - public class CommandAttribute : Attribute - { - public string[] CommandNames { get; } - public string? Description { get; } - - public CommandAttribute(string commandName) - : this(new[] { commandName }, null) - { - } - - public CommandAttribute(string commandName, string description) - : this(new[] { commandName }, description) - { - } - - public CommandAttribute(string[] commandNames) - : this(commandNames, null) - { - } - - public CommandAttribute(string[] commandNames, string? description) - { - this.CommandNames = commandNames; - this.Description = description; - } - } - - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] - public class RootCommandAttribute : Attribute - { - } -} \ No newline at end of file diff --git a/src/ConsoleAppFramework/CommandDescriptor.cs b/src/ConsoleAppFramework/CommandDescriptor.cs deleted file mode 100644 index e88a180..0000000 --- a/src/ConsoleAppFramework/CommandDescriptor.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using System.Linq; -using System.Reflection; - -namespace ConsoleAppFramework -{ - internal enum CommandType - { - DefaultCommand, - Command, - SubCommand - } - - internal class CommandDescriptor - { - public CommandType CommandType { get; } - public MethodInfo MethodInfo { get; } - public object? Instance { get; } - public CommandAttribute? CommandAttribute { get; } - public string? ParentCommand { get; } - - public string[] GetNames(ConsoleAppOptions options) - { - if (CommandAttribute != null) return CommandAttribute.CommandNames; - return new[] { options.NameConverter(MethodInfo.Name) }; - } - - public string GetNamesFormatted(ConsoleAppOptions options) - { - return string.Join(", ", GetNames(options)); - } - - public string[] Aliases - { - get - { - if (CommandAttribute == null || CommandAttribute.CommandNames.Length <= 1) - { - return Array.Empty(); - } - else - { - return CommandAttribute.CommandNames.Skip(1).ToArray(); - } - } - } - - public string GetCommandName(ConsoleAppOptions options) - { - if (ParentCommand != null) - { - return $"{ParentCommand} {GetNamesFormatted(options)}"; - } - else - { - return GetNamesFormatted(options); - } - } - - public string Description - { - get - { - if (CommandAttribute != null) - { - return CommandAttribute.Description ?? ""; - } - else - { - return ""; - } - } - } - - public CommandDescriptor(CommandType commandType, MethodInfo methodInfo, object? instance = null, CommandAttribute? additionalCommandAttribute = null, string? parentCommand = null) - { - CommandType = commandType; - MethodInfo = methodInfo; - Instance = instance; - CommandAttribute = additionalCommandAttribute ?? methodInfo.GetCustomAttribute(); - ParentCommand = parentCommand; - } - } -} \ No newline at end of file diff --git a/src/ConsoleAppFramework/CommandDescriptorCollection.cs b/src/ConsoleAppFramework/CommandDescriptorCollection.cs deleted file mode 100644 index 725642e..0000000 --- a/src/ConsoleAppFramework/CommandDescriptorCollection.cs +++ /dev/null @@ -1,184 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; - -namespace ConsoleAppFramework -{ - internal class CommandDescriptorCollection - { - CommandDescriptor? rootCommandDescriptor; - readonly Dictionary descriptors = new Dictionary(StringComparer.OrdinalIgnoreCase); - readonly Dictionary> subCommandDescriptors = new Dictionary>(StringComparer.OrdinalIgnoreCase); - readonly ConsoleAppOptions options; - - public CommandDescriptorCollection(ConsoleAppOptions options) - { - this.options = options; - } - - public void AddCommand(CommandDescriptor commandDescriptor) - { - foreach (var name in commandDescriptor.GetNames(options)) - { - if (subCommandDescriptors.ContainsKey(name) || !descriptors.TryAdd(name, commandDescriptor)) - { - throw new InvalidOperationException($"Duplicate command name is added. Name:{name} Method:{commandDescriptor.MethodInfo.DeclaringType?.Name}.{commandDescriptor.MethodInfo.Name}"); - } - } - } - - public void AddSubCommand(string parentCommand, CommandDescriptor commandDescriptor) - { - if (descriptors.ContainsKey(parentCommand)) - { - throw new InvalidOperationException($"Duplicate parent-command is added. Name:{parentCommand} Method:{commandDescriptor.MethodInfo.DeclaringType?.Name}.{commandDescriptor.MethodInfo.Name}"); - } - - if (!subCommandDescriptors.TryGetValue(parentCommand, out var commandDict)) - { - commandDict = new Dictionary(StringComparer.OrdinalIgnoreCase); - subCommandDescriptors.Add(parentCommand, commandDict); - } - - foreach (var name in commandDescriptor.GetNames(options)) - { - if (!commandDict.TryAdd(name, commandDescriptor)) - { - throw new InvalidOperationException($"Duplicate command name is added. Name:{parentCommand} {name} Method:{commandDescriptor.MethodInfo.DeclaringType?.Name}.{commandDescriptor.MethodInfo.Name}"); - } - } - } - - public void AddRootCommand(CommandDescriptor commandDescriptor) - { - if (this.rootCommandDescriptor != null) - { - throw new InvalidOperationException($"Found more than one root command. Method:{rootCommandDescriptor.MethodInfo.DeclaringType?.Name}.{rootCommandDescriptor.MethodInfo.Name} and {commandDescriptor.MethodInfo.DeclaringType?.Name}.{commandDescriptor.MethodInfo.Name}"); - } - - this.rootCommandDescriptor = commandDescriptor; - } - - // Only check command name(not foo) - public bool TryGetDescriptor(string[] args, [MaybeNullWhen(false)] out CommandDescriptor descriptor, out int offset) - { - // 1. Try to match sub command - if (args.Length >= 2) - { - if (subCommandDescriptors.TryGetValue(args[0], out var dict)) - { - if (dict.TryGetValue(args[1], out descriptor)) - { - offset = 2; - return true; - } - else - { - goto NOTMATCH; - } - } - } - - // 2. Try to match command - if (args.Length >= 1) - { - if (descriptors.TryGetValue(args[0], out descriptor)) - { - offset = 1; - return true; - } - } - - // 3. default - if (rootCommandDescriptor != null) - { - offset = 0; - descriptor = rootCommandDescriptor; - return true; - } - - // not match. - NOTMATCH: - offset = 0; - descriptor = default; - return false; - } - - public void TryAddDefaultHelpMethod() - { - // add if not exists. - descriptors.TryAdd(DefaultCommands.Help, DefaultCommands.HelpCommand); - } - - public void TryAddDefaultVersionMethod() - { - descriptors.TryAdd(DefaultCommands.Version, DefaultCommands.VersionCommand); - } - - public bool TryGetHelpMethod([MaybeNullWhen(false)] out CommandDescriptor commandDescriptor) - { - return descriptors.TryGetValue(DefaultCommands.Help, out commandDescriptor); - } - - public bool TryGetVersionMethod([MaybeNullWhen(false)] out CommandDescriptor commandDescriptor) - { - return descriptors.TryGetValue(DefaultCommands.Version, out commandDescriptor); - } - - public CommandDescriptor? GetRootCommandDescriptor() => rootCommandDescriptor; - - /// - /// GetAll(except default) descriptors. - /// - public IEnumerable GetAllDescriptors() - { - IEnumerable IterateCore() - { - foreach (var item in descriptors.Values) - { - yield return item; - } - foreach (var item in subCommandDescriptors.Values) - { - foreach (var item2 in item.Values) - { - yield return item2; - } - } - } - - return IterateCore().Distinct(); - } - - public CommandDescriptor[] GetSubCommands(string rootCommand) - { - if (subCommandDescriptors.TryGetValue(rootCommand, out var dict)) - { - return dict.Values.Distinct().ToArray(); - } - - return Array.Empty(); - } - - class TupleStringComparer : IEqualityComparer<(string, string)> - { - public bool Equals((string, string) x, (string, string) y) - { - if (StringComparer.OrdinalIgnoreCase.Equals(x.Item1, y.Item1)) - { - if (StringComparer.OrdinalIgnoreCase.Equals(x.Item2, y.Item2)) - { - return true; - } - } - return false; - } - - public int GetHashCode((string, string) obj) - { - return (StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Item1), StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Item2)).GetHashCode(); - } - } - } -} \ No newline at end of file diff --git a/src/ConsoleAppFramework/CommandHelpBuilder.cs b/src/ConsoleAppFramework/CommandHelpBuilder.cs index 667c309..cf4e72d 100644 --- a/src/ConsoleAppFramework/CommandHelpBuilder.cs +++ b/src/ConsoleAppFramework/CommandHelpBuilder.cs @@ -1,239 +1,246 @@ -using Microsoft.Extensions.DependencyInjection; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; +using Microsoft.CodeAnalysis; using System.Text; -namespace ConsoleAppFramework +namespace ConsoleAppFramework; + +public static class CommandHelpBuilder { - internal class CommandHelpBuilder + public static string BuildRootHelpMessage(Command command) { - readonly Func getExecutionCommandName; - readonly bool isStrictOption; - readonly IServiceProviderIsService isService; - readonly ConsoleAppOptions options; + return BuildHelpMessageCore(command, showCommandName: false, showCommand: false); + } + + public static string BuildRootHelpMessage(Command[] commands) + { + var sb = new StringBuilder(); + + var rootCommand = commands.FirstOrDefault(x => x.IsRootCommand); + var withoutRoot = commands.Where(x => !x.IsRootCommand).ToArray(); - public CommandHelpBuilder(Func? getExecutionCommandName, IServiceProviderIsService isService, ConsoleAppOptions options) + if (rootCommand != null && withoutRoot.Length == 0) { - this.getExecutionCommandName = getExecutionCommandName ?? GetExecutionCommandNameDefault; - this.isStrictOption = options.StrictOption; - this.isService = isService; - this.options = options; + return BuildRootHelpMessage(commands[0]); } - private string GetExecutionCommandNameDefault() + if (rootCommand != null) { - if (options.ApplicationName != null) - { - return options.ApplicationName; - } - else - { - return Path.GetFileNameWithoutExtension(Assembly.GetEntryAssembly()!.Location); - } + sb.AppendLine(BuildHelpMessageCore(rootCommand, false, withoutRoot.Length != 0)); + } + else + { + sb.AppendLine("Usage: [command] [-h|--help] [--version]"); + sb.AppendLine(); } - public string GetExecutionCommandName() => getExecutionCommandName(); + if (withoutRoot.Length == 0) return sb.ToString(); - public string BuildHelpMessage(CommandDescriptor? defaultCommand, IEnumerable commands, bool shortCommandName) - { - var sb = new StringBuilder(); + var helpDefinitions = withoutRoot.OrderBy(x => x.CommandFullName).ToArray(); - bool showHeader = (defaultCommand != null); - if (defaultCommand != null) - { - // Display a help messages for default method - sb.Append(BuildHelpMessage(CreateCommandHelpDefinition(defaultCommand, shortCommandName), showCommandName: false, fromMultiCommand: false)); - } + var list = BuildMethodListMessage(helpDefinitions, out _); + sb.Append(list); - var orderedCommands = options.HelpSortCommandsByFullName - ? commands.OrderBy(x => x.GetCommandName(options)).ToArray() - : commands.OrderBy(x => x.GetNamesFormatted(options)).ToArray(); - if (orderedCommands.Length > 0) - { - if (defaultCommand == null) - { - sb.Append(BuildUsageMessage()); - sb.AppendLine(); - } + return sb.ToString(); + } - sb.Append(BuildMethodListMessage(orderedCommands, shortCommandName, out var maxWidth)); - } + public static string BuildCommandHelpMessage(Command command) + { + return BuildHelpMessageCore(command, showCommandName: command.CommandName != "", showCommand: false); + } - return sb.ToString(); - } + static string BuildHelpMessageCore(Command command, bool showCommandName, bool showCommand) + { + var definition = CreateCommandHelpDefinition(command); - public string BuildHelpMessage(CommandDescriptor command) + var sb = new StringBuilder(); + + sb.AppendLine(BuildUsageMessage(definition, showCommandName, showCommand)); + + if (!string.IsNullOrEmpty(definition.Description)) { - return BuildHelpMessage(CreateCommandHelpDefinition(command, false), showCommandName: false, fromMultiCommand: false); + sb.AppendLine(); + sb.AppendLine(definition.Description); } - internal string BuildHelpMessage(CommandHelpDefinition definition, bool showCommandName, bool fromMultiCommand) + if (definition.Options.Any()) { - var sb = new StringBuilder(); - - sb.AppendLine(BuildUsageMessage(definition, showCommandName, fromMultiCommand)); - sb.AppendLine(); + var hasArgument = definition.Options.Any(x => x.Index.HasValue); + var hasOptions = definition.Options.Any(x => !x.Index.HasValue); - if (!string.IsNullOrEmpty(definition.Description)) + if (hasArgument) { - sb.AppendLine(definition.Description); sb.AppendLine(); + sb.AppendLine(BuildArgumentsMessage(definition)); } - if (definition.Options.Any()) + if (hasOptions) { - sb.Append(BuildArgumentsMessage(definition)); - sb.Append(BuildOptionsMessage(definition)); - } - else - { - sb.AppendLine("Options:"); - sb.AppendLine(" ()"); sb.AppendLine(); + sb.AppendLine(BuildOptionsMessage(definition)); } - - return sb.ToString(); } - internal string BuildUsageMessage() - { - var sb = new StringBuilder(); - sb.AppendLine($"Usage: {GetExecutionCommandName()} "); + return sb.ToString(); + } + + static string BuildUsageMessage(CommandHelpDefinition definition, bool showCommandName, bool showCommand) + { + var sb = new StringBuilder(); + sb.Append($"Usage:"); - return sb.ToString(); + if (showCommandName) + { + sb.Append($" {definition.CommandName}"); } - internal string BuildUsageMessage(CommandHelpDefinition definition, bool showCommandName, bool fromMultiCommand) + if (showCommand) { - var sb = new StringBuilder(); - sb.Append($"Usage: {GetExecutionCommandName()}"); + sb.Append(" [command]"); + } - if (showCommandName) - { - sb.Append($" {(definition.CommandAliases.Any() ? ((fromMultiCommand ? definition.Command + " " : "") + definition.CommandAliases[0]) : definition.Command)}"); - } + if (definition.Options.Any(x => x.Index.HasValue)) + { + sb.Append(" [arguments...]"); + } - foreach (var opt in definition.Options.Where(x => x.Index.HasValue)) - { - sb.Append($" <{(string.IsNullOrEmpty(opt.Description) ? opt.Options[0] : opt.Description)}>"); - } + if (definition.Options.Any(x => !x.Index.HasValue)) + { + sb.Append(" [options...]"); + } - if (definition.Options.Any(x => !x.Index.HasValue)) - { - sb.Append(" [options...]"); - } + sb.Append(" [-h|--help] [--version]"); - return sb.ToString(); - } + return sb.ToString(); + } - internal string BuildArgumentsMessage(CommandHelpDefinition definition) - { - var argumentsFormatted = definition.Options - .Where(x => x.Index.HasValue) - .Select(x => (Argument: $"[{x.Index}] {x.FormattedValueTypeName}", x.Description)) - .ToArray(); + static string BuildArgumentsMessage(CommandHelpDefinition definition) + { + var argumentsFormatted = definition.Options + .Where(x => x.Index.HasValue) + .Select(x => (Argument: $"[{x.Index}] {x.FormattedValueTypeName}", x.Description)) + .ToArray(); - if (!argumentsFormatted.Any()) return string.Empty; + if (!argumentsFormatted.Any()) return string.Empty; - var maxWidth = argumentsFormatted.Max(x => x.Argument.Length); + var maxWidth = argumentsFormatted.Max(x => x.Argument.Length); - var sb = new StringBuilder(); + var sb = new StringBuilder(); - sb.AppendLine("Arguments:"); - foreach (var arg in argumentsFormatted) + sb.AppendLine("Arguments:"); + var first = true; + foreach (var arg in argumentsFormatted) + { + if (first) { - var padding = maxWidth - arg.Argument.Length; + first = false; + } + else + { + sb.AppendLine(); + } + var padding = maxWidth - arg.Argument.Length; - sb.Append(" "); - sb.Append(arg.Argument); + sb.Append(" "); + sb.Append(arg.Argument); + if (!string.IsNullOrEmpty(arg.Description)) + { for (var i = 0; i < padding; i++) { sb.Append(' '); } sb.Append(" "); - sb.AppendLine(arg.Description); + sb.Append(arg.Description); } - - sb.AppendLine(); - - return sb.ToString(); } - internal string BuildOptionsMessage(CommandHelpDefinition definition) - { - var optionsFormatted = definition.Options - .Where(x => !x.Index.HasValue) - .Select(x => (Options: string.Join(", ", x.Options) + (x.IsFlag ? string.Empty : $" {x.FormattedValueTypeName}"), x.Description, x.IsRequired, x.IsFlag, x.DefaultValue)) - .ToArray(); + return sb.ToString(); + } - if (!optionsFormatted.Any()) return string.Empty; + static string BuildOptionsMessage(CommandHelpDefinition definition) + { + var optionsFormatted = definition.Options + .Where(x => !x.Index.HasValue) + .Select(x => (Options: string.Join("|", x.Options) + (x.IsFlag ? string.Empty : $" {x.FormattedValueTypeName}{(x.IsParams ? "..." : "")}"), x.Description, x.IsRequired, x.IsFlag, x.DefaultValue)) + .ToArray(); - var maxWidth = optionsFormatted.Max(x => x.Options.Length); + if (!optionsFormatted.Any()) return string.Empty; - var sb = new StringBuilder(); + var maxWidth = optionsFormatted.Max(x => x.Options.Length); - sb.AppendLine("Options:"); - foreach (var opt in optionsFormatted) - { - var options = opt.Options; - var padding = maxWidth - options.Length; + var sb = new StringBuilder(); - sb.Append(" "); - sb.Append(options); - for (var i = 0; i < padding; i++) - { - sb.Append(' '); - } - - sb.Append(" "); - sb.Append(opt.Description); + sb.AppendLine("Options:"); + var first = true; + foreach (var opt in optionsFormatted) + { + if (first) + { + first = false; + } + else + { + sb.AppendLine(); + } - if (opt.IsFlag) - { - sb.Append($" (Optional)"); - } - else if (opt.DefaultValue != null) - { - sb.Append($" (Default: {opt.DefaultValue})"); - } - else if (opt.IsRequired) - { - sb.Append($" (Required)"); - } + var options = opt.Options; + var padding = maxWidth - options.Length; - sb.AppendLine(); + sb.Append(" "); + sb.Append(options); + for (var i = 0; i < padding; i++) + { + sb.Append(' '); } - sb.AppendLine(); + sb.Append(" "); + sb.Append(opt.Description); - return sb.ToString(); + if (opt.IsFlag) + { + sb.Append($" (Optional)"); + } + else if (opt.DefaultValue != null) + { + sb.Append($" (Default: {opt.DefaultValue})"); + } + else if (opt.IsRequired) + { + sb.Append($" (Required)"); + } } - internal string BuildMethodListMessage(IEnumerable types, bool shortCommandName, out int maxWidth) - { - maxWidth = 0; - return BuildMethodListMessage(types.Select(x => CreateCommandHelpDefinition(x, shortCommandName)), true, out maxWidth); - } + return sb.ToString(); + } - internal string BuildMethodListMessage(IEnumerable commandHelpDefinitions, bool appendCommand, out int maxWidth) - { - var formatted = commandHelpDefinitions - .Select(x => (Command: $"{(x.CommandAliases.Length != 0 ? ((appendCommand ? x.Command + " " : "") + string.Join(", ", x.CommandAliases)) : x.Command)}", Description: x.Description)) - .ToArray(); - maxWidth = formatted.Max(x => x.Command.Length); + static string BuildMethodListMessage(IEnumerable commands, out int maxWidth) + { + var formatted = commands + .Select(x => + { + var full = x.CommandName; + if (x.CommandPath.Length > 0) + { + full = string.Join(" ", x.CommandPath) + " " + x.CommandName; + } - var sb = new StringBuilder(); + return (Command: full, x.Description); + }) + .ToArray(); + maxWidth = formatted.Max(x => x.Command.Length); - sb.AppendLine("Commands:"); - foreach (var item in formatted) - { - sb.Append(" "); - sb.Append(item.Command); + var sb = new StringBuilder(); + sb.AppendLine("Commands:"); + foreach (var item in formatted) + { + sb.Append(" "); + sb.Append(item.Command); + if (string.IsNullOrEmpty(item.Description)) + { + sb.AppendLine(); + } + else + { var padding = maxWidth - item.Command.Length; for (var i = 0; i < padding; i++) { @@ -243,148 +250,114 @@ internal string BuildMethodListMessage(IEnumerable comman sb.Append(" "); sb.AppendLine(item.Description); } - - return sb.ToString(); } - internal CommandHelpDefinition CreateCommandHelpDefinition(CommandDescriptor descriptor, bool shortCommandName) - { - var parameterDefinitions = new List(); - - foreach (var item in descriptor.MethodInfo.GetParameters()) - { - // ignore DI params. - if (item.ParameterType == typeof(ConsoleAppContext) || (isService != null && isService.IsService(item.ParameterType))) continue; - - // -i, -input | [default=foo]... + return sb.ToString(); + } - var index = default(int?); - var itemName = this.options.NameConverter(item.Name!); + static CommandHelpDefinition CreateCommandHelpDefinition(Command descriptor) + { + var parameterDefinitions = new List(); - var options = new List(); - var option = item.GetCustomAttribute(); - if (option != null) - { - if (option.Index != -1) - { - index = option.Index; - options.Add($"[{option.Index}]"); - } - else - { - // If Index is -1, ShortName is initialized at Constractor. - if (option.ShortName != null) - { - options.Add($"-{option.ShortName.Trim('-')}"); - } - } - } + foreach (var item in descriptor.Parameters) + { + // ignore DI params. + if (!item.IsParsable) continue; - if (!index.HasValue) - { - if (isStrictOption) - { - options.Add($"--{itemName}"); - } - else - { - options.Add($"-{itemName}"); - } - } + // -i, -input | [default=foo]... - var description = string.Empty; - if (option != null && !string.IsNullOrEmpty(option.Description)) - { - description = option.Description ?? string.Empty; - } - else + var index = item.ArgumentIndex == -1 ? null : (int?)item.ArgumentIndex; + var options = new List(); + if (item.ArgumentIndex != -1) + { + options.Add($"[{item.ArgumentIndex}]"); + } + else + { + // aliases first + foreach (var alias in item.Aliases) { - description = string.Empty; + options.Add(alias); } + options.Add("--" + item.Name); + } - var isFlag = item.ParameterType == typeof(bool); + var description = item.Description; + var isFlag = item.Type.SpecialType == Microsoft.CodeAnalysis.SpecialType.System_Boolean; + var isParams = item.IsParams; - var defaultValue = default(string); - if (item.HasDefaultValue) + var defaultValue = default(string); + if (item.HasDefaultValue) + { + defaultValue = item.DefaultValue == null ? "null" : item.DefaultValueToString(castValue: false, enumIncludeTypeName: false); + if (isFlag) { - if (option?.DefaultValue != null) + if (item.DefaultValue is true) { - defaultValue = option.DefaultValue; + // bool option with true default value is not flag. + isFlag = false; } - else + else if (item.DefaultValue is false) { - defaultValue = (item.DefaultValue?.ToString() ?? "null"); + // false default value should be omitted for flag. + defaultValue = null; } - if (isFlag) - { - if (item.DefaultValue is true) - { - // bool option with true default value is not flag. - isFlag = false; - } - else if (item.DefaultValue is false) - { - // false default value should be omitted for flag. - defaultValue = null; - } - } - } - - var paramTypeName = item.ParameterType.Name; - if (item.ParameterType.IsGenericType && item.ParameterType.GetGenericTypeDefinition() == typeof(Nullable<>)) - { - paramTypeName = item.ParameterType.GetGenericArguments()[0].Name + "?"; } - - parameterDefinitions.Add(new CommandOptionHelpDefinition(options.Distinct().ToArray(), description, paramTypeName, defaultValue, index, isFlag)); } - return new CommandHelpDefinition( - shortCommandName ? descriptor.GetNamesFormatted(options) : descriptor.GetCommandName(options), - descriptor.Aliases, - parameterDefinitions.OrderBy(x => x.Index ?? int.MaxValue).ToArray(), - descriptor.Description - ); + var paramTypeName = item.ToTypeShortString(); + parameterDefinitions.Add(new CommandOptionHelpDefinition(options.Distinct().ToArray(), description, paramTypeName, defaultValue, index, isFlag, isParams)); } - public class CommandHelpDefinition + var commandName = descriptor.CommandName; + if (descriptor.CommandPath.Length != 0) { - public string Command { get; } - public string[] CommandAliases { get; } - public CommandOptionHelpDefinition[] Options { get; } - public string Description { get; } - - public CommandHelpDefinition(string command, string[] commandAliases, CommandOptionHelpDefinition[] options, string description) - { - Command = command; - CommandAliases = commandAliases; - Options = options; - Description = description; - } + commandName = string.Join(" ", descriptor.CommandPath) + " " + descriptor.CommandName; } - public class CommandOptionHelpDefinition - { - public string[] Options { get; } - public string Description { get; } - public string? DefaultValue { get; } - public string ValueTypeName { get; } - public int? Index { get; } + return new CommandHelpDefinition( + commandName, + parameterDefinitions.ToArray(), + descriptor.Description + ); + } - public bool IsRequired => DefaultValue == null; - public bool IsFlag { get; } - public string FormattedValueTypeName => "<" + ValueTypeName + ">"; + class CommandHelpDefinition + { + public string CommandName { get; } + public CommandOptionHelpDefinition[] Options { get; } + public string Description { get; } - public CommandOptionHelpDefinition(string[] options, string description, string valueTypeName, string? defaultValue, int? index, bool isFlag) - { - Options = options; - Description = description; - ValueTypeName = valueTypeName; - DefaultValue = defaultValue; - Index = index; - IsFlag = isFlag; - } + public CommandHelpDefinition(string command, CommandOptionHelpDefinition[] options, string description) + { + CommandName = command; + Options = options; + Description = description; } + } + class CommandOptionHelpDefinition + { + public string[] Options { get; } + public string Description { get; } + public string? DefaultValue { get; } + public string ValueTypeName { get; } + public int? Index { get; } + + public bool IsRequired => DefaultValue == null && !IsParams; + public bool IsFlag { get; } + public bool IsParams { get; } + public string FormattedValueTypeName => "<" + ValueTypeName + ">"; + + public CommandOptionHelpDefinition(string[] options, string description, string valueTypeName, string? defaultValue, int? index, bool isFlag, bool isParams) + { + Options = options; + Description = description; + ValueTypeName = valueTypeName; + DefaultValue = defaultValue; + Index = index; + IsFlag = isFlag; + IsParams = isParams; + } } -} +} \ No newline at end of file diff --git a/src/ConsoleAppFramework/ConsoleApp.cs b/src/ConsoleAppFramework/ConsoleApp.cs deleted file mode 100644 index 00746c3..0000000 --- a/src/ConsoleAppFramework/ConsoleApp.cs +++ /dev/null @@ -1,255 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; - -namespace ConsoleAppFramework -{ - public class ConsoleApp - { - // Keep this reference as ConsoleApOptions.CommandDescriptors. - readonly CommandDescriptorCollection commands; - readonly ConsoleAppOptions options; - - public IHost Host { get; } - public ILogger Logger { get; } - public IServiceProvider Services => Host.Services; - public IConfiguration Configuration => Host.Services.GetRequiredService(); - public IHostEnvironment Environment => Host.Services.GetRequiredService(); - public IHostApplicationLifetime Lifetime => Host.Services.GetRequiredService(); - - internal ConsoleApp(IHost host) - { - this.Host = host; - this.Logger = host.Services.GetRequiredService>(); - this.options = host.Services.GetRequiredService(); - this.commands = options.CommandDescriptors; - } - - // Statics - - public static ConsoleApp Create(string[] args) - { - return CreateBuilder(args).Build(); - } - - public static ConsoleApp Create(string[] args, Action configureOptions) - { - return CreateBuilder(args, configureOptions).Build(); - } - - public static ConsoleApp Create(string[] args, Action configureOptions) - { - return CreateBuilder(args, configureOptions).Build(); - } - - public static ConsoleAppBuilder CreateBuilder(string[] args) - { - return new ConsoleAppBuilder(args, Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder(args)); - } - - public static ConsoleAppBuilder CreateBuilder(string[] args, Action configureOptions) - { - return new ConsoleAppBuilder(args, Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder(args), configureOptions); - } - - public static ConsoleAppBuilder CreateBuilder(string[] args, Action configureOptions) - { - return new ConsoleAppBuilder(args, Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder(args), configureOptions); - } - - public static ConsoleApp CreateFromHostBuilder(IHostBuilder hostBuilder, string[] args) - { - return new ConsoleAppBuilder(args, hostBuilder).Build(); - } - - public static ConsoleApp CreateFromHostBuilder(IHostBuilder hostBuilder, string[] args, Action configureOptions) - { - return new ConsoleAppBuilder(args, hostBuilder, configureOptions).Build(); - } - - public static ConsoleApp CreateFromHostBuilder(IHostBuilder hostBuilder, string[] args, Action configureOptions) - { - return new ConsoleAppBuilder(args, hostBuilder, configureOptions).Build(); - } - - public static void Run(string[] args, Delegate rootCommand) - { - RunAsync(args, rootCommand).GetAwaiter().GetResult(); - } - - public static Task RunAsync(string[] args, Delegate rootCommand) - { - return Create(args).AddRootCommand(rootCommand).RunAsync(); - } - - public static void Run(string[] args) - where T : ConsoleAppBase - { - Create(args).AddCommands().Run(); - } - - // Add Command - - public ConsoleApp AddRootCommand(Delegate command) - { - var attr = command.Method.GetCustomAttribute(); - commands.AddRootCommand(new CommandDescriptor(CommandType.DefaultCommand, command.Method, command.Target, attr)); - return this; - } - - public ConsoleApp AddRootCommand(string description, Delegate command) - { - var attr = new CommandAttribute("root-command", description); - commands.AddRootCommand(new CommandDescriptor(CommandType.DefaultCommand, command.Method, command.Target, attr)); - return this; - } - - public ConsoleApp AddCommand(string commandName, Delegate command) - { - var attr = new CommandAttribute(commandName); - commands.AddCommand(new CommandDescriptor(CommandType.Command, command.Method, command.Target, attr)); - return this; - } - - public ConsoleApp AddCommand(string commandName, string description, Delegate command) - { - var attr = new CommandAttribute(commandName, description); - commands.AddCommand(new CommandDescriptor(CommandType.Command, command.Method, command.Target, attr)); - return this; - } - - public ConsoleApp AddCommands() - where T : ConsoleAppBase - { - var methods = typeof(T).GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); - foreach (var method in methods) - { - if (method.Name == "Dispose" || method.Name == "DisposeAsync") continue; // ignore IDisposable - - if (method.GetCustomAttribute() != null || (options.NoAttributeCommandAsImplicitlyDefault && method.GetCustomAttribute() == null)) - { - var command = new CommandDescriptor(CommandType.DefaultCommand, method); - commands.AddRootCommand(command); - } - else - { - var command = new CommandDescriptor(CommandType.Command, method); - commands.AddCommand(command); - } - } - return this; - } - - public ConsoleApp AddSubCommand(string parentCommandName, string commandName, Delegate command) - { - var attr = new CommandAttribute(commandName); - commands.AddSubCommand(parentCommandName, new CommandDescriptor(CommandType.SubCommand, command.Method, command.Target, attr, parentCommandName)); - return this; - } - - public ConsoleApp AddSubCommand(string parentCommandName, string commandName, string description, Delegate command) - { - var attr = new CommandAttribute(commandName, description); - commands.AddSubCommand(parentCommandName, new CommandDescriptor(CommandType.SubCommand, command.Method, command.Target, attr, parentCommandName)); - return this; - } - - public ConsoleApp AddSubCommands() - { - var methods = typeof(T).GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); - - var rootName = typeof(T).GetCustomAttribute()?.CommandNames[0] ?? options.NameConverter(typeof(T).Name); - - foreach (var method in methods) - { - if (method.Name == "Dispose" || method.Name == "DisposeAsync") continue; // ignore IDisposable - - if (method.GetCustomAttribute() != null) - { - var command = new CommandDescriptor(CommandType.DefaultCommand, method); - commands.AddRootCommand(command); - } - else - { - var command = new CommandDescriptor(CommandType.SubCommand, method, parentCommand: rootName); - commands.AddSubCommand(rootName, command); - } - } - return this; - } - - public ConsoleApp AddAllCommandType() - { - return AddAllCommandType(AppDomain.CurrentDomain.GetAssemblies()); - } - - public ConsoleApp AddAllCommandType(params Assembly[] searchAssemblies) - { - foreach (var type in GetConsoleAppTypes(searchAssemblies)) - { - var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); - var rootName = type.GetCustomAttribute()?.CommandNames[0] ?? options.NameConverter(type.Name); - foreach (var method in methods) - { - if (method.Name == "Dispose" || method.Name == "DisposeAsync") continue; // ignore IDisposable - - commands.AddSubCommand(rootName, new CommandDescriptor(CommandType.SubCommand, method, parentCommand: rootName)); - } - } - return this; - } - - // Run - - public void Run() - { - RunAsync().GetAwaiter().GetResult(); - } - - // Don't use return RunAsync to keep stacktrace. - public async Task RunAsync(CancellationToken cancellationToken = default) - { - commands.TryAddDefaultHelpMethod(); - commands.TryAddDefaultVersionMethod(); - - await Host.RunAsync(cancellationToken); - } - - static List GetConsoleAppTypes(Assembly[] searchAssemblies) - { - List consoleAppBaseTypes = new List(); - - foreach (var asm in searchAssemblies) - { - if (asm.FullName!.StartsWith("System") || asm.FullName.StartsWith("Microsoft.Extensions") || asm.GetName().Name == "ConsoleAppFramework") continue; - - Type?[] types; - try - { - types = asm.GetTypes(); - } - catch (ReflectionTypeLoadException ex) - { - types = ex.Types ?? Array.Empty(); - } - - foreach (var item in types.Where(x => x != null)) - { - if (typeof(ConsoleAppBase).IsAssignableFrom(item) && item != typeof(ConsoleAppBase)) - { - consoleAppBaseTypes.Add(item!); - } - } - } - - return consoleAppBaseTypes; - } - } -} \ No newline at end of file diff --git a/src/ConsoleAppFramework/ConsoleAppBase.cs b/src/ConsoleAppFramework/ConsoleAppBase.cs deleted file mode 100644 index a491735..0000000 --- a/src/ConsoleAppFramework/ConsoleAppBase.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace ConsoleAppFramework -{ - public abstract class ConsoleAppBase - { - // Context will be set non-null value by ConsoleAppEngine, - // but it might be null because it has public setter. - #nullable disable warnings - public ConsoleAppContext Context { get; set; } - #nullable restore warnings - } -} diff --git a/src/ConsoleAppFramework/ConsoleAppBuilder.cs b/src/ConsoleAppFramework/ConsoleAppBuilder.cs deleted file mode 100644 index 5ad8191..0000000 --- a/src/ConsoleAppFramework/ConsoleAppBuilder.cs +++ /dev/null @@ -1,223 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; - -namespace ConsoleAppFramework -{ - public class ConsoleAppBuilder : IHostBuilder - { - readonly IHostBuilder builder; - - internal ConsoleAppBuilder(string[] args, IHostBuilder hostBuilder) - : this(args, hostBuilder, (_, __) => { }) - { - } - - internal ConsoleAppBuilder(string[] args, IHostBuilder hostBuilder, ConsoleAppOptions consoleAppOptions) - { - this.builder = AddConsoleAppFramework(hostBuilder, args, consoleAppOptions, null); - } - - internal ConsoleAppBuilder(string[] args, IHostBuilder hostBuilder, Action configureOptions) - : this(args, hostBuilder, (_, options) => configureOptions(options)) - { - } - - internal ConsoleAppBuilder(string[] args, IHostBuilder hostBuilder, Action configureOptions) - { - this.builder = AddConsoleAppFramework(hostBuilder, args, new ConsoleAppOptions(), configureOptions); - } - - IHostBuilder AddConsoleAppFramework(IHostBuilder builder, string[] args, ConsoleAppOptions options, Action? configureOptions) - { - return builder - .ConfigureServices((ctx, services) => - { - services.AddOptions().Configure(x => x.SuppressStatusMessages = true); - services.AddHostedService(); - configureOptions?.Invoke(ctx, options); - options.CommandLineArguments = args; - services.AddSingleton(options); - services.AddSingleton(); - - if (options.ReplaceToUseSimpleConsoleLogger) - { - services.AddLogging(builder => - { - builder.ReplaceToSimpleConsole(); - }); - } - }) - .UseConsoleLifetime(); - } - - // IHostBuilder implementations:: - - public IDictionary Properties => builder.Properties; - - IHost IHostBuilder.Build() - { - return builder.Build(); - } - - IHostBuilder IHostBuilder.ConfigureAppConfiguration(Action configureDelegate) - { - return builder.ConfigureAppConfiguration(configureDelegate); - } - - IHostBuilder IHostBuilder.ConfigureContainer(Action configureDelegate) - { - return builder.ConfigureContainer(configureDelegate); - } - - IHostBuilder IHostBuilder.ConfigureHostConfiguration(Action configureDelegate) - { - return builder.ConfigureHostConfiguration(configureDelegate); - } - - IHostBuilder IHostBuilder.ConfigureServices(Action configureDelegate) - { - return builder.ConfigureServices(configureDelegate); - } - - IHostBuilder IHostBuilder.UseServiceProviderFactory(IServiceProviderFactory factory) - { - return builder.UseServiceProviderFactory(factory); - } - - IHostBuilder IHostBuilder.UseServiceProviderFactory(Func> factory) - { - return builder.UseServiceProviderFactory(factory); - } - - // override implementations that returns ConsoleAppBuilder - - public ConsoleApp Build() - { - var host = builder.Build(); - return new ConsoleApp(host); - } - - public ConsoleAppBuilder ConfigureAppConfiguration(Action configureDelegate) - { - builder.ConfigureAppConfiguration(configureDelegate); - return this; - } - - public ConsoleAppBuilder ConfigureContainer(Action configureDelegate) - { - builder.ConfigureContainer(configureDelegate); - return this; - } - - public ConsoleAppBuilder ConfigureHostConfiguration(Action configureDelegate) - { - builder.ConfigureHostConfiguration(configureDelegate); - return this; - } - - public ConsoleAppBuilder ConfigureServices(Action configureDelegate) - { - builder.ConfigureServices(configureDelegate); - return this; - } - - public ConsoleAppBuilder UseServiceProviderFactory(IServiceProviderFactory factory) - where TContainerBuilder : notnull - { - builder.UseServiceProviderFactory(factory); - return this; - } - - public ConsoleAppBuilder UseServiceProviderFactory(Func> factory) - where TContainerBuilder : notnull - { - builder.UseServiceProviderFactory(factory); - return this; - } - - // Override Configure methods(Microsoft.Extensions.Hosting.HostingHostBuilderExtensions) tor return ConsoleAppBuilder - - public ConsoleAppBuilder ConfigureLogging(Action configureLogging) - { - (this as IHostBuilder).ConfigureLogging(configureLogging); - return this; - } - - public ConsoleAppBuilder ConfigureLogging(Action configureLogging) - { - (this as IHostBuilder).ConfigureLogging(configureLogging); - return this; - } - - public ConsoleAppBuilder UseEnvironment(string environment) - { - (this as IHostBuilder).UseEnvironment(environment); - return this; - } - - public ConsoleAppBuilder UseContentRoot(string contentRoot) - { - (this as IHostBuilder).UseContentRoot(contentRoot); - return this; - } - - public ConsoleAppBuilder UseDefaultServiceProvider(Action configure) - { - (this as IHostBuilder).UseDefaultServiceProvider(configure); - return this; - } - - public ConsoleAppBuilder UseDefaultServiceProvider(Action configure) - { - (this as IHostBuilder).UseDefaultServiceProvider(configure); - return this; - } - - public ConsoleAppBuilder ConfigureHostOptions(Action configureOptions) - { - (this as IHostBuilder).ConfigureHostOptions(configureOptions); - return this; - } - - public ConsoleAppBuilder ConfigureHostOptions(Action configureOptions) - { - (this as IHostBuilder).ConfigureHostOptions(configureOptions); - return this; - } - - public ConsoleAppBuilder ConfigureAppConfiguration(Action configureDelegate) - { - (this as IHostBuilder).ConfigureAppConfiguration(configureDelegate); - return this; - } - - public ConsoleAppBuilder ConfigureServices(Action configureDelegate) - { - (this as IHostBuilder).ConfigureServices(configureDelegate); - return this; - } - - public ConsoleAppBuilder ConfigureContainer(Action configureDelegate) - { - (this as IHostBuilder).ConfigureContainer(configureDelegate); - return this; - } - } - - public static class HostBuilderExtensions - { - public static ConsoleApp BuildAsConsoleApp(this IHostBuilder hostBuilder) - { - var app = hostBuilder.Build() as ConsoleApp; - if (app == null) - { - throw new InvalidOperationException($"HostBuilder is not ConsoleAppBuilder."); - } - return app; - } - } -} \ No newline at end of file diff --git a/src/ConsoleAppFramework/ConsoleAppContext.cs b/src/ConsoleAppFramework/ConsoleAppContext.cs deleted file mode 100644 index 804df87..0000000 --- a/src/ConsoleAppFramework/ConsoleAppContext.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Reflection; -using System.Threading; - -namespace ConsoleAppFramework -{ - public class ConsoleAppContext - { - readonly CancellationTokenSource cancellationTokenSource; - - public string?[] Arguments { get; } - public DateTime Timestamp { get; } - public CancellationToken CancellationToken { get; } - public ILogger Logger { get; } - public MethodInfo MethodInfo { get; } - public IServiceProvider ServiceProvider { get; } - public IDictionary Items { get; } - - public ConsoleAppContext(string?[] arguments, DateTime timestamp, CancellationTokenSource cancellationTokenSource, ILogger logger, MethodInfo methodInfo, IServiceProvider serviceProvider) - { - this.cancellationTokenSource = cancellationTokenSource; - Arguments = arguments; - Timestamp = timestamp; - CancellationToken = cancellationTokenSource.Token; - Logger = logger; - MethodInfo = methodInfo; - ServiceProvider = serviceProvider; - Items = new Dictionary(); - } - - public void Cancel() - { - cancellationTokenSource.Cancel(); - } - - public void Terminate() - { - cancellationTokenSource.Cancel(); - cancellationTokenSource.Token.ThrowIfCancellationRequested(); - } - } -} diff --git a/src/ConsoleAppFramework/ConsoleAppEngine.cs b/src/ConsoleAppFramework/ConsoleAppEngine.cs deleted file mode 100644 index cebd57d..0000000 --- a/src/ConsoleAppFramework/ConsoleAppEngine.cs +++ /dev/null @@ -1,518 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Collections.ObjectModel; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Reflection; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; - -namespace ConsoleAppFramework -{ - internal class ConsoleAppEngine - { - readonly ILogger logger; - readonly IServiceProvider provider; - readonly CancellationTokenSource cancellationTokenSource; - readonly ConsoleAppOptions options; - readonly IServiceProviderIsService isService; - readonly IParamsValidator paramsValidator; - readonly bool isStrict; - - public ConsoleAppEngine(ILogger logger, - IServiceProvider provider, - ConsoleAppOptions options, - IServiceProviderIsService isService, - IParamsValidator paramsValidator, - CancellationTokenSource cancellationTokenSource) - { - this.logger = logger; - this.provider = provider; - this.paramsValidator = paramsValidator; - this.cancellationTokenSource = cancellationTokenSource; - this.options = options; - this.isService = isService; - this.isStrict = options.StrictOption; - } - - public async Task RunAsync() - { - logger.LogTrace("ConsoleAppEngine.Run Start"); - - var args = options.CommandLineArguments; - - if (!options.CommandDescriptors.TryGetDescriptor(args, out var commandDescriptor, out var offset)) - { - if (args.Length == 0) - { - if (options.CommandDescriptors.TryGetHelpMethod(out commandDescriptor)) - { - goto RUN; - } - } - - // TryGet Single help or Version - if (args.Length == 1) - { - switch (args[0].Trim('-')) - { - case "help": - if (options.CommandDescriptors.TryGetHelpMethod(out commandDescriptor)) - { - goto RUN; - } - break; - case "version": - if (options.CommandDescriptors.TryGetVersionMethod(out commandDescriptor)) - { - goto RUN; - } - break; - default: - break; - } - } - - // TryGet SubCommands Help - if (args.Length >= 2 && args[1].Trim('-') == "help") - { - var subCommands = options.CommandDescriptors.GetSubCommands(args[0]); - if (subCommands.Length != 0) - { - var msg = new CommandHelpBuilder(() => args[0], isService, options).BuildHelpMessage(null, subCommands, shortCommandName: true); - Console.WriteLine(msg); - return; - } - } - - await SetFailAsync("Command not found. args: " + string.Join(" ", args)); - return; - } - - // foo --help - // foo bar --help - if (args.Skip(offset).FirstOrDefault()?.Trim('-') == "help") - { - var msg = new CommandHelpBuilder(() => commandDescriptor.GetCommandName(options), isService, options).BuildHelpMessage(commandDescriptor); - Console.WriteLine(msg); - return; - } - - // check can invoke help - if (commandDescriptor.CommandType == CommandType.DefaultCommand && args.Length == 0) - { - var p = commandDescriptor.MethodInfo.GetParameters(); - if (p.Any(x => !(x.ParameterType == typeof(ConsoleAppContext) || isService.IsService(x.ParameterType) || x.HasDefaultValue))) - { - options.CommandDescriptors.TryGetHelpMethod(out commandDescriptor); - } - } - - RUN: - await RunCore(commandDescriptor!.MethodInfo!.DeclaringType!, commandDescriptor.MethodInfo, commandDescriptor.Instance, args, offset); - } - - // Try to invoke method. - async Task RunCore(Type type, MethodInfo methodInfo, object? instance, string?[] args, int argsOffset) - { - object?[] invokeArgs; - ParameterInfo[] originalParameters = methodInfo.GetParameters(); - var isService = provider.GetService(); - try - { - var parameters = originalParameters; - if (isService != null) - { - parameters = parameters.Where(x => !(x.ParameterType == typeof(ConsoleAppContext) || isService.IsService(x.ParameterType))).ToArray(); - } - - if (!TryGetInvokeArguments(parameters, args, argsOffset, out invokeArgs, out var errorMessage)) - { - await SetFailAsync(errorMessage + " args: " + string.Join(" ", args)); - return; - } - - } - catch (Exception ex) - { - await SetFailAsync("Fail to match method parameter on " + type.Name + "." + methodInfo.Name + ". args: " + string.Join(" ", args), ex); - return; - } - - var ctx = new ConsoleAppContext(args, DateTime.UtcNow, cancellationTokenSource, logger, methodInfo, provider); - - // re:create invokeArgs, merge with DI parameter. - if (invokeArgs.Length != originalParameters.Length) - { - var newInvokeArgs = new object?[originalParameters.Length]; - var invokeArgsIndex = 0; - for (int i = 0; i < originalParameters.Length; i++) - { - var p = originalParameters[i].ParameterType; - if (p == typeof(ConsoleAppContext)) - { - newInvokeArgs[i] = ctx; - } - else if (isService!.IsService(p)) - { - try - { - newInvokeArgs[i] = provider.GetService(p); - } - catch (Exception ex) - { - await SetFailAsync("Fail to get service parameter. ParameterType:" + p.FullName, ex); - return; - } - } - else - { - newInvokeArgs[i] = invokeArgs[invokeArgsIndex++]; - } - } - invokeArgs = newInvokeArgs; - } - - var validationResult = paramsValidator.ValidateParameters(originalParameters.Zip(invokeArgs)); - if (validationResult != ValidationResult.Success) - { - await SetFailAsync(validationResult!.ErrorMessage!); - return; - } - - try - { - if (instance == null && !type.IsAbstract && !methodInfo.IsStatic) - { - instance = ActivatorUtilities.CreateInstance(provider, type); - typeof(ConsoleAppBase).GetProperty(nameof(ConsoleAppBase.Context))!.SetValue(instance, ctx); - } - - } - catch (Exception ex) - { - await SetFailAsync("Fail to create ConsoleAppBase instance. Type:" + type.FullName, ex); - return; - } - - try - { - var invoker = new WithFilterInvoker(methodInfo, instance, invokeArgs, provider, options.GlobalFilters ?? Array.Empty(), ctx); - try - { - var result = await invoker.InvokeAsync(); - if (result != null) - { - Environment.ExitCode = result.Value; - } - } - finally - { - if (instance is IAsyncDisposable ad) - { - await ad.DisposeAsync(); - } - else if (instance is IDisposable d) - { - d.Dispose(); - } - } - } - catch (Exception ex) - { - if (ex is TargetInvocationException tex) - { - ex = tex.InnerException ?? tex; - } - - if (ex is OperationCanceledException operationCanceledException && operationCanceledException.CancellationToken == cancellationTokenSource.Token) - { - // NOTE: Do nothing if the exception has thrown by the CancellationToken of ConsoleAppEngine. - // If the user code throws OperationCanceledException, ConsoleAppEngine should not handle that. - return; - } - - await SetFailAsync("Fail in application running on " + type.Name + "." + methodInfo.Name, ex); - return; - } - - logger.LogTrace("ConsoleAppEngine.Run Complete Successfully"); - } - - ValueTask SetFailAsync(string message) - { - Environment.ExitCode = 1; - logger.LogError(message); - return default; - } - - ValueTask SetFailAsync(string message, Exception? ex) - { - Environment.ExitCode = 1; - logger.LogError(ex, message); - return default; - } - - bool TryGetInvokeArguments(ParameterInfo[] parameters, string?[] args, int argsOffset, out object?[] invokeArgs, out string? errorMessage) - { - try - { - var jsonOption = options.JsonSerializerOptions; - - // Collect option types for parsing command-line arguments. - var optionTypeByOptionName = new Dictionary(StringComparer.OrdinalIgnoreCase); - for (int i = 0; i < parameters.Length; i++) - { - var item = parameters[i]; - var option = item.GetCustomAttribute(); - - optionTypeByOptionName[(isStrict ? "--" : "") + options.NameConverter(item.Name!)] = item.ParameterType; - if (!string.IsNullOrWhiteSpace(option?.ShortName)) - { - optionTypeByOptionName[(isStrict ? "-" : "") + option!.ShortName!] = item.ParameterType; - } - } - - var (argumentDictionary, optionByIndex) = ParseArgument(args, argsOffset, optionTypeByOptionName, isStrict); - invokeArgs = new object[parameters.Length]; - - for (int i = 0; i < parameters.Length; i++) - { - var item = parameters[i]; - var itemName = options.NameConverter(item.Name!); - var option = item.GetCustomAttribute(); - if (!string.IsNullOrWhiteSpace(option?.ShortName) && char.IsDigit(option!.ShortName, 0)) throw new InvalidOperationException($"Option '{itemName}' has a short name, but the short name must start with A-Z or a-z."); - - var value = default(OptionParameter); - - // Indexed arguments (e.g. [Option(0)]) - if (option != null && option.Index != -1) - { - if (optionByIndex.Count <= option.Index) - { - if (!item.HasDefaultValue) - { - throw new InvalidOperationException($"Required argument {option.Index} was not found in specified arguments."); - } - } - else - { - value = optionByIndex[option.Index]; - } - } - - // Keyed options (e.g. -foo -bar ) - var longName = (isStrict) ? ("--" + itemName) : itemName; - var shortName = (isStrict) ? ("-" + option?.ShortName?.TrimStart('-')) : option?.ShortName?.TrimStart('-'); - - if (value.Value != null || argumentDictionary.TryGetValue(longName!, out value) || argumentDictionary.TryGetValue(shortName ?? "", out value)) - { - if (parameters[i].ParameterType == typeof(bool) && value.Value == null) - { - invokeArgs[i] = value.BooleanSwitch; - continue; - } - - if (value.Value != null) - { - if (parameters[i].ParameterType == typeof(string)) - { - // when string, invoke directly(avoid JSON escape) - invokeArgs[i] = value.Value; - continue; - } - else if (parameters[i].ParameterType.IsEnum) - { - try - { - invokeArgs[i] = Enum.Parse(parameters[i].ParameterType, value.Value, true); - continue; - } - catch - { - errorMessage = "Parameter \"" + itemName + "\"" + " fail on Enum parsing."; - return false; - } - } - else if (typeof(System.Collections.IEnumerable).IsAssignableFrom(parameters[i].ParameterType) && !typeof(System.Collections.IDictionary).IsAssignableFrom(parameters[i].ParameterType)) - { - var v = value.Value; - if (!(v.StartsWith("[") && v.EndsWith("]"))) - { - var elemType = UnwrapCollectionElementType(parameters[i].ParameterType); - if (elemType == typeof(string)) - { - if (!(v.StartsWith("\"") && v.EndsWith("\""))) - { - v = "[" + string.Join(",", v.Split(' ', ',').Select(x => "\"" + x + "\"")) + "]"; - } - else - { - v = "[" + v + "]"; - } - } - else - { - v = "[" + string.Join(",", v.Trim('\'', '\"').Split(' ', ',')) + "]"; - } - } - try - { - invokeArgs[i] = JsonSerializer.Deserialize(v, parameters[i].ParameterType, jsonOption); - continue; - } - catch - { - errorMessage = "Parameter \"" + itemName + "\"" + " fail on JSON deserialize, please check type or JSON escape or add double-quotation."; - return false; - } - } - else - { - var v = value.Value; - try - { - try - { - invokeArgs[i] = JsonSerializer.Deserialize(v, parameters[i].ParameterType, jsonOption); - continue; - } - catch (JsonException) - { - // retry with double quotations - if (!(v.StartsWith("\"") && v.EndsWith("\""))) - { - v = $"\"{v}\""; - } - invokeArgs[i] = JsonSerializer.Deserialize(v, parameters[i].ParameterType, jsonOption); - continue; - } - } - catch - { - errorMessage = "Parameter \"" + itemName + "\"" + " fail on JSON deserialize, please check type or JSON escape or add double-quotation."; - return false; - } - } - } - } - - if (item.HasDefaultValue) - { - invokeArgs[i] = item.DefaultValue; - } - else if (item.ParameterType == typeof(bool)) - { - // bool without default value should be considered that it has implicit default value of false. - invokeArgs[i] = false; - } - else - { - var name = itemName; - if (option?.ShortName != null) - { - name = itemName + "(" + "-" + option.ShortName + ")"; - } - errorMessage = "Required parameter \"" + name + "\"" + " not found in argument."; - return false; - } - } - - errorMessage = null; - return true; - } - catch (Exception ex) - { - invokeArgs = default!; - errorMessage = ex.Message; - return false; - } - } - - static Type? UnwrapCollectionElementType(Type collectionType) - { - if (collectionType.IsArray) - { - return collectionType.GetElementType(); - } - - foreach (var i in collectionType.GetInterfaces()) - { - if (i.IsGenericType && (i.GetGenericTypeDefinition() == typeof(IEnumerable<>))) - { - return i.GetGenericArguments()[0]; - } - } - - return null; - } - - static (ReadOnlyDictionary OptionByKey, IReadOnlyList OptionByIndex) ParseArgument(string?[] args, int argsOffset, IReadOnlyDictionary optionTypeByName, bool isStrict) - { - var dict = new Dictionary(args.Length, StringComparer.OrdinalIgnoreCase); - var options = new List(); - for (int i = argsOffset; i < args.Length;) - { - var arg = args[i++]; - if (arg is null || !arg.StartsWith("-")) - { - options.Add(new OptionParameter() { Value = arg }); - continue; // not key - } - - var key = (isStrict) ? arg : arg.TrimStart('-'); - - if (optionTypeByName.TryGetValue(key, out var optionType)) - { - if (optionType == typeof(bool)) - { - var boolValue = true; - if (i < args.Length) - { - var isTrue = args[i]?.Equals("true", StringComparison.OrdinalIgnoreCase); - var isFalse = args[i]?.Equals("false", StringComparison.OrdinalIgnoreCase); - if (isTrue != null && isTrue.Value) - { - boolValue = true; - } - else if (isFalse != null && isFalse.Value) - { - boolValue = false; - } - } - - dict.Add(key, new OptionParameter { BooleanSwitch = boolValue }); - } - else - { - if (args.Length <= i) - { - throw new ArgumentException($@"Value for parameter ""{key}"" is not provided."); - } - - var value = args[i]; - dict.Add(key, new OptionParameter { Value = value }); - i++; - } - } - else - { - // not key - options.Add(new OptionParameter() { Value = arg }); - } - } - - return (new ReadOnlyDictionary(dict), options); - } - - struct OptionParameter - { - public string? Value; - public bool BooleanSwitch; - } - } -} \ No newline at end of file diff --git a/src/ConsoleAppFramework/ConsoleAppEngineService.cs b/src/ConsoleAppFramework/ConsoleAppEngineService.cs deleted file mode 100644 index fdf14b0..0000000 --- a/src/ConsoleAppFramework/ConsoleAppEngineService.cs +++ /dev/null @@ -1,132 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace ConsoleAppFramework -{ - // This servcie is called from ConsoleApp.Run - internal sealed class ConsoleAppEngineService : IHostedService - { - IHostApplicationLifetime appLifetime; - ILogger logger; - IServiceProvider provider; - IServiceScope? scope; - Task? runningTask; - CancellationTokenSource? cancellationTokenSource; - - public ConsoleAppEngineService(IHostApplicationLifetime appLifetime, ILogger logger, IServiceProvider provider, IOptionsMonitor hostOptions) - { - this.appLifetime = appLifetime; - this.provider = provider; - this.logger = logger; - } - - public Task StartAsync(CancellationToken ct) - { - cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(ct); - - // raise after all event registered - appLifetime.ApplicationStarted.Register(async state => - { - var self = (ConsoleAppEngineService)state!; - try - { - self.scope = self.provider.CreateScope(); - if (self.cancellationTokenSource == null) self.cancellationTokenSource = new CancellationTokenSource(); - var engine = ActivatorUtilities.CreateInstance(self.scope.ServiceProvider, self.cancellationTokenSource!); - self.runningTask = engine.RunAsync(); - await self.runningTask; - self.runningTask = null; - } - catch - { - // don't do anything. - } - finally - { - self.appLifetime.StopApplication(); - } - }, this); - - // call from Ctrl+C, etc... - appLifetime.ApplicationStopping.Register(state => - { - var cts = (CancellationTokenSource?)state; - cts?.Cancel(); - }, cancellationTokenSource); - - return Task.CompletedTask; - } - - public async Task StopAsync(CancellationToken ct) - { - try - { - cancellationTokenSource?.Cancel(); - - var task = runningTask; - if (task != null) - { - logger.LogTrace("Detect Cancel signal, wait for running console app task canceled."); - try - { - if (ct.CanBeCanceled) - { - var cancelTask = CreateTimeoutTask(ct); - var completedTask = await Task.WhenAny(cancelTask, task); - if (completedTask == cancelTask) - { - logger.LogTrace("ConsoleApp aborted, cancel timeout."); - } - else - { - logger.LogTrace("ConsoleApp cancel completed."); - } - } - else - { - await task; - logger.LogTrace("ConsoleApp cancel completed."); - } - } - catch (OperationCanceledException ex) - { - if (ex.CancellationToken == ct) - { - logger.LogTrace("ConsoleApp aborted, cancel timeout."); - } - else - { - logger.LogTrace("ConsoleApp cancel completed."); - } - } - } - } - finally - { - if (scope is IAsyncDisposable asyncDisposable) - { - await asyncDisposable.DisposeAsync(); - } - else - { - scope?.Dispose(); - } - } - } - - Task CreateTimeoutTask(CancellationToken ct) - { - var tcs = new TaskCompletionSource(); - ct.Register(() => - { - tcs.TrySetCanceled(ct); - }); - return tcs.Task; - } - } -} \ No newline at end of file diff --git a/src/ConsoleAppFramework/ConsoleAppFilter.cs b/src/ConsoleAppFramework/ConsoleAppFilter.cs deleted file mode 100644 index 95c0d9a..0000000 --- a/src/ConsoleAppFramework/ConsoleAppFilter.cs +++ /dev/null @@ -1,122 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -namespace ConsoleAppFramework -{ - public abstract class ConsoleAppFilter - { - public int Order { get; set; } - public abstract ValueTask Invoke(ConsoleAppContext context, Func next); - } - - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] - public class ConsoleAppFilterAttribute : Attribute - { - public Type Type { get; } - public int Order { get; set; } - - public ConsoleAppFilterAttribute(Type type) - { - this.Type = type; - } - } - - internal class FilterRunner - { - readonly ConsoleAppFilter filter; - readonly Func next; - - public FilterRunner(ConsoleAppFilter filter, Func next) - { - this.filter = filter; - this.next = next; - } - - public Func GetDelegate() => InvokeAsync; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - ValueTask InvokeAsync(ConsoleAppContext context) - { - return filter.Invoke(context, next); - } - } - - internal class WithFilterInvoker - { - readonly MethodInfo methodInfo; - readonly object? instance; - readonly object?[] invokeArgs; - readonly IServiceProvider serviceProvider; - readonly ConsoleAppFilter[] globalFilters; - readonly ConsoleAppContext context; - - int? invokeResult; - - public WithFilterInvoker(MethodInfo methodInfo, object? instance, object?[] invokeArgs, IServiceProvider serviceProvider, ConsoleAppFilter[] globalFilters, ConsoleAppContext context) - { - this.methodInfo = methodInfo; - this.instance = instance; - this.invokeArgs = invokeArgs; - this.serviceProvider = serviceProvider; - this.globalFilters = globalFilters; - this.context = context; - } - - public async ValueTask InvokeAsync() - { - var list = new List(globalFilters); - - var classFilters = methodInfo.DeclaringType!.GetCustomAttributes(true); - var methodFilters = methodInfo.GetCustomAttributes(true); - foreach (var item in classFilters.Concat(methodFilters)) - { - var filter = (ConsoleAppFilter) ActivatorUtilities.CreateInstance(serviceProvider, item.Type); - filter.Order = item.Order; - list.Add(filter); - } - - var sortedAndReversedFilters = list.OrderBy(x => x.Order).Reverse().ToArray(); - - Func next = RunCore; - foreach (var f in sortedAndReversedFilters) - { - next = new FilterRunner(f, next).GetDelegate(); - } - - await next(context); - return invokeResult; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - async ValueTask RunCore(ConsoleAppContext _) - { - var result = methodInfo.Invoke(instance, invokeArgs); - if (result != null) - { - switch (result) - { - case int exitCode: - invokeResult = exitCode; - break; - case Task taskWithExitCode: - invokeResult = await taskWithExitCode; - break; - case Task task: - await task; - break; - case ValueTask valueTaskWithExitCode: - invokeResult = await valueTaskWithExitCode; - break; - case ValueTask valueTask: - await valueTask; - break; - } - } - } - } -} \ No newline at end of file diff --git a/src/ConsoleAppFramework/ConsoleAppFramework.csproj b/src/ConsoleAppFramework/ConsoleAppFramework.csproj index 33046f7..b6c00c5 100644 --- a/src/ConsoleAppFramework/ConsoleAppFramework.csproj +++ b/src/ConsoleAppFramework/ConsoleAppFramework.csproj @@ -1,33 +1,26 @@ - - - netcoreapp3.1;net5.0;net6.0 - 8.0 - enable - true - release.snk - true + - - ConsoleAppFramework - Micro-framework for console applications. - true - + - 1701;1702;1591 - + netstandard2.0 + 12 + enable + enable + ConsoleAppFramework - - - - + true + cs + - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - - - + + diff --git a/src/ConsoleAppFramework/ConsoleAppFramework.props b/src/ConsoleAppFramework/ConsoleAppFramework.props deleted file mode 100644 index 6fb1fc5..0000000 --- a/src/ConsoleAppFramework/ConsoleAppFramework.props +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs b/src/ConsoleAppFramework/ConsoleAppGenerator.cs similarity index 99% rename from src/ConsoleAppFramework5/ConsoleAppGenerator.cs rename to src/ConsoleAppFramework/ConsoleAppGenerator.cs index 4f78ab0..d35609d 100644 --- a/src/ConsoleAppFramework5/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework/ConsoleAppGenerator.cs @@ -594,7 +594,7 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex if (filter == null) { sourceProductionContext.ReportDiagnostic(DiagnosticDescriptors.FilterMultipleConsturtor, genericType.GetLocation()); - return null; + return null!; } return filter!; diff --git a/src/ConsoleAppFramework/ConsoleAppOptions.cs b/src/ConsoleAppFramework/ConsoleAppOptions.cs deleted file mode 100644 index 7d8999a..0000000 --- a/src/ConsoleAppFramework/ConsoleAppOptions.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using System.Text; -using System.Text.Json; - -namespace ConsoleAppFramework -{ - public class ConsoleAppOptions - { - /// - /// Argument parser uses strict(-short, --long) option. Default is true. - /// - public bool StrictOption { get; set; } = true; - - /// - /// Show default command(help/version) to help. Default is true. - /// - public bool ShowDefaultCommand { get; set; } = true; - public bool ReplaceToUseSimpleConsoleLogger { get; set; } = true; - public JsonSerializerOptions? JsonSerializerOptions { get; set; } - - public ConsoleAppFilter[]? GlobalFilters { get; set; } - - public bool NoAttributeCommandAsImplicitlyDefault { get; set; } - - public Func NameConverter { get; set; } = KebabCaseConvert; - - public bool HelpSortCommandsByFullName { get; set; } = false; - - public string? ApplicationName { get; set; } = null; - - // internal store values for execute engine. - - internal string[] CommandLineArguments { get; set; } = default!; - internal CommandDescriptorCollection CommandDescriptors { get; } - - public ConsoleAppOptions() - { - CommandDescriptors = new CommandDescriptorCollection(this); - } - - public static ConsoleAppOptions CreateLegacyCompatible() - { - return new ConsoleAppOptions() - { - StrictOption = false, - NoAttributeCommandAsImplicitlyDefault = true, - NameConverter = x => x.ToLower(), - ReplaceToUseSimpleConsoleLogger = false - }; - } - - static string KebabCaseConvert(string name) - { - var sb = new StringBuilder(); - for (int i = 0; i < name.Length; i++) - { - if (!Char.IsUpper(name[i])) - { - sb.Append(name[i]); - continue; - } - - // Abc, abC, AB-c => first or Last or capital continuous, no added. - if (i == 0 || i == name.Length - 1 || Char.IsUpper(name[i + 1])) - { - sb.Append(Char.ToLowerInvariant(name[i])); - continue; - } - - // others, add- - sb.Append('-'); - sb.Append(Char.ToLowerInvariant(name[i])); - } - return sb.ToString(); - } - } -} \ No newline at end of file diff --git a/src/ConsoleAppFramework/DefaultCommands.cs b/src/ConsoleAppFramework/DefaultCommands.cs deleted file mode 100644 index 0f5f5e0..0000000 --- a/src/ConsoleAppFramework/DefaultCommands.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Text; - -namespace ConsoleAppFramework -{ - internal class DefaultCommands : ConsoleAppBase - { - public const string Help = "help"; - public const string Version = "version"; - - public static readonly CommandDescriptor HelpCommand = new CommandDescriptor(CommandType.Command, typeof(DefaultCommands).GetMethod(nameof(ShowHelp), BindingFlags.Public | BindingFlags.Instance)!); - public static readonly CommandDescriptor VersionCommand = new CommandDescriptor(CommandType.Command, typeof(DefaultCommands).GetMethod(nameof(ShowVersion), BindingFlags.Public | BindingFlags.Instance)!); - - readonly ConsoleAppOptions options; - readonly IServiceProviderIsService isService; - - public DefaultCommands(ConsoleAppOptions options, IServiceProviderIsService isService) - { - this.options = options; - this.isService = isService; - } - - [Command("help", "Display help.")] - public void ShowHelp() - { - var descriptors = options.CommandDescriptors.GetAllDescriptors(); - if (!options.ShowDefaultCommand) - { - descriptors = descriptors.Where(x => x != HelpCommand && x != VersionCommand); - } - var message = new CommandHelpBuilder(null,isService, options).BuildHelpMessage(options.CommandDescriptors.GetRootCommandDescriptor(), descriptors, shortCommandName: false); - Console.WriteLine(message); - } - - [Command("version", "Display version.")] - public void ShowVersion() - { - 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; - } - } - Console.WriteLine(version); - } - } -} \ No newline at end of file diff --git a/src/ConsoleAppFramework5/DiagnosticDescriptors.cs b/src/ConsoleAppFramework/DiagnosticDescriptors.cs similarity index 100% rename from src/ConsoleAppFramework5/DiagnosticDescriptors.cs rename to src/ConsoleAppFramework/DiagnosticDescriptors.cs diff --git a/src/ConsoleAppFramework5/Emitter.cs b/src/ConsoleAppFramework/Emitter.cs similarity index 100% rename from src/ConsoleAppFramework5/Emitter.cs rename to src/ConsoleAppFramework/Emitter.cs diff --git a/src/ConsoleAppFramework/Icon.png b/src/ConsoleAppFramework/Icon.png deleted file mode 100644 index 68d64b1e0e5c10200e85a15ab96a0b43ff989f79..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3185 zcmd5;4NOy46uwx2tpaMaI1$0bD9cb;bOT$#Jv;m%3Mq&bl}?@1ZbI#}yg3*kV$i6I z%n`;CD5ao)fT(j?N@H=+un42n1zIT}0voTZSeaE+qNwO&Ktb}oNr~~ah!x^+s2{b zQJT#p9z!#Dn}bPg-tNur*=7=(f94I8n;Z7DGJ!F;JsR0GpKXCJ2wBF-AI@M@4;CEg z40{3fzuNuRjb6C?pO!`}3iB-|$VuTgYD%^by4@hT@-G9N@-2^|z&EGVUn5#=d|~eF zRN85^^M$yBUuQg8Ct_*YK0(aY`5jN@SabYEyWIyGuZYCG?6TVQaz;j|GH@2-w!vS} zWVLK}CeHJ7gDNzG0T)fb9I1uj#P++D%YZkNm7suPuSHRlb%Nzup_eD~?uGj&wJ_fg z)G@k#LQ6X<&Bwc?^tq(Rv^Z+$miNh5p$fWo`UITPg2_4s z^o`Qg#@gr`aHw~aP&|=ns{=pAf*DMCW~?%BeZukvU*QvtP9+laEd94HE8J8yD$kk% z=>x$#S97ZL9Y@r~1B$sL;Vry@w(yV?P`1onHO*A3ulr>4WNBwZ$r~dTQv;DMnNzot z9vQ4=Ez}sF)M%_c@#{y3T%=lVo>&a9_)J8WjG=*py?t8DVRE{pIXDGnxIj0kM^nCi zB?xr9F8R^a=rdxlCX#BcGo5-ReHdTKN6RCQlChO@?4$P zw{qyVyh2MzDc8b8K-)1`qz3k}H?r8mL>^$4paNB-jx?ac_2OJc^$^CO)ErD4lBxfW zk>Q+AfH{B8#Gy7O0B9hDdw;3xGea=3p$5YggQ*7-yA`J|dL3;XRz2qFDbTUe0R%b= zt7lmXMC^cq8tPe!XScg~kgAI=6`pWs{hfM##3Qy<+xOdEWT-Hq55oxQN~vi<7YnIT z^ZFd`kKK_bcGk^^mzd_o)w`xu34{6Mtz3a`7R7}d;J zy*6U0ki*hzbH1i006}IFs^3w@ksd_eFzJUR^c70-E8Ntf({b#p@X|=Jn7TlNSc2=+ zvus?$jOe)ur;ZGkLf~MKyp3uB@uh@ot<9PE4sI0P&s#}*b9a4(JX5gEkgw6D^DG?; nKbDmhcKT)eOmzPo{ZI~Qz{z@ji^5RskI;mzjtH%0Z_oS(cSy02 diff --git a/src/ConsoleAppFramework/LegacyCompatibleExtensions.cs b/src/ConsoleAppFramework/LegacyCompatibleExtensions.cs deleted file mode 100644 index 4617e7e..0000000 --- a/src/ConsoleAppFramework/LegacyCompatibleExtensions.cs +++ /dev/null @@ -1,86 +0,0 @@ -using Microsoft.Extensions.Hosting; -using System; -using System.ComponentModel; -using System.Reflection; -using System.Threading.Tasks; - -namespace ConsoleAppFramework -{ - public static class LegacyCompatibleExtensions - { - /// - /// Run multiple ConsoleApp that are searched from all assemblies. - /// - // [Obsolete] - [EditorBrowsable(EditorBrowsableState.Never)] - public static Task RunConsoleAppFrameworkAsync(this IHostBuilder hostBuilder, string[] args, ConsoleAppOptions? options = null, Assembly[]? searchAssemblies = null) - { - options = ConfigureLegacyCompatible(options); - args = ConfigureLegacyCompatibleArgs(args); - - return new ConsoleAppBuilder(args, hostBuilder, options) - .Build() - .AddAllCommandType(searchAssemblies ?? AppDomain.CurrentDomain.GetAssemblies()) - .RunAsync(); - } - - /// - /// Run a single ConsoleApp type that is targeted by type argument. - /// - // [Obsolete] - [EditorBrowsable(EditorBrowsableState.Never)] - public static Task RunConsoleAppFrameworkAsync(this IHostBuilder hostBuilder, string[] args, ConsoleAppOptions? options = null) - where T : ConsoleAppBase - { - options = ConfigureLegacyCompatible(options); - args = ConfigureLegacyCompatibleArgs(args); - - return new ConsoleAppBuilder(args, hostBuilder, options) - .Build() - .AddCommands() - .RunAsync(); - } - - static ConsoleAppOptions ConfigureLegacyCompatible(ConsoleAppOptions? options) - { - if (options == null) - { - options = new ConsoleAppOptions(); - } - - options.NoAttributeCommandAsImplicitlyDefault = true; - options.StrictOption = false; - options.NameConverter = x => x.ToLower(); - options.ReplaceToUseSimpleConsoleLogger = false; - return options; - } - - static string[] ConfigureLegacyCompatibleArgs(string[] args) - { - if (args.Length >= 1 && args[0].Contains(".")) - { - var spritCommand = args[0].Split('.'); - - var newArgs = new string[args.Length + 1]; - for (int i = 0; i < newArgs.Length; i++) - { - if (i == 0) - { - newArgs[i] = spritCommand[0]; - } - else if (i == 1) - { - newArgs[i] = spritCommand[1]; - } - else - { - newArgs[i] = args[i - 1]; - } - } - return newArgs; - } - - return args; - } - } -} \ No newline at end of file diff --git a/src/ConsoleAppFramework5/NameConverter.cs b/src/ConsoleAppFramework/NameConverter.cs similarity index 100% rename from src/ConsoleAppFramework5/NameConverter.cs rename to src/ConsoleAppFramework/NameConverter.cs diff --git a/src/ConsoleAppFramework/OptionAttribute.cs b/src/ConsoleAppFramework/OptionAttribute.cs deleted file mode 100644 index 4b5950a..0000000 --- a/src/ConsoleAppFramework/OptionAttribute.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; - -namespace ConsoleAppFramework -{ - [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] - public class OptionAttribute : Attribute - { - public int Index { get; } - public string? ShortName { get; } - public string? Description { get; } - - /// Override default value on help. - public string? DefaultValue { get; set; } - - public OptionAttribute(int index) - { - this.Index = index; - this.Description = null; - } - - public OptionAttribute(int index, string description) - { - this.Index = index; - this.Description = description; - } - - public OptionAttribute(string shortName) - { - this.Index = -1; - this.ShortName = string.IsNullOrWhiteSpace(shortName) ? null : shortName; - this.Description = null; - } - - public OptionAttribute(string? shortName, string description) - { - this.Index = -1; - this.ShortName = string.IsNullOrWhiteSpace(shortName) ? null : shortName; - this.Description = description; - } - } -} diff --git a/src/ConsoleAppFramework/ParamsValidator.cs b/src/ConsoleAppFramework/ParamsValidator.cs deleted file mode 100644 index 9474cb4..0000000 --- a/src/ConsoleAppFramework/ParamsValidator.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Reflection; - -namespace ConsoleAppFramework -{ - /// - /// Validator of command parameters. - /// - public interface IParamsValidator - { - /// - /// Validate of command based on validation attributes - /// applied to method's parameters. - /// - ValidationResult? ValidateParameters(IEnumerable<(ParameterInfo Parameter, object? Value)> parameters); - } - - /// - public class ParamsValidator : IParamsValidator - { - private readonly ConsoleAppOptions options; - - public ParamsValidator(ConsoleAppOptions options) => this.options = options; - - /// - ValidationResult? IParamsValidator.ValidateParameters( - IEnumerable<(ParameterInfo Parameter, object? Value)> parameters) - { - var invalidParameters = parameters - .Select(tuple => (tuple.Parameter, tuple.Value, Result: Validate(tuple.Parameter, tuple.Value))) - .Where(tuple => tuple.Result != ValidationResult.Success) - .ToImmutableArray(); - - if (!invalidParameters.Any()) - { - return ValidationResult.Success; - } - - var errorMessage = string.Join(Environment.NewLine, - invalidParameters - .Select(tuple => - $"{options.NameConverter(tuple.Parameter.Name!)} " + - $"({tuple.Value}): " + - $"{tuple.Result!.ErrorMessage}") - ); - - return new ValidationResult($"Some parameters have invalid values:{Environment.NewLine}{errorMessage}"); - } - - private static ValidationResult? Validate(ParameterInfo parameterInfo, object? value) - { - if (value is null) return ValidationResult.Success; - - var validationContext = new ValidationContext(value, null, null); - - var failedResults = GetValidationAttributes(parameterInfo) - .Select(attribute => attribute.GetValidationResult(value, validationContext)) - .Where(result => result != ValidationResult.Success) - .ToImmutableArray(); - - return failedResults.Any() - ? new ValidationResult(string.Join("; ", failedResults.Select(res => res?.ErrorMessage))) - : ValidationResult.Success; - } - - private static IEnumerable GetValidationAttributes(ParameterInfo parameterInfo) - => parameterInfo - .GetCustomAttributes() - .OfType(); - } -} diff --git a/src/ConsoleAppFramework5/Parser.cs b/src/ConsoleAppFramework/Parser.cs similarity index 95% rename from src/ConsoleAppFramework5/Parser.cs rename to src/ConsoleAppFramework/Parser.cs index 8a98e1f..bfee114 100644 --- a/src/ConsoleAppFramework5/Parser.cs +++ b/src/ConsoleAppFramework/Parser.cs @@ -117,22 +117,19 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta var hasIAsyncDisposable = type.AllInterfaces.Any(x => SymbolEqualityComparer.Default.Equals(x, wellKnownTypes.IAsyncDisposable)); var typeFilters = type.GetAttributes() + .Where(x => x.AttributeClass?.Name == "ConsoleAppFilterAttribute") .Select(x => { - if (x.AttributeClass?.Name == "ConsoleAppFilterAttribute") - { - var filterType = x.AttributeClass.TypeArguments[0]; - var filter = FilterInfo.Create(filterType); - - if (filter == null) - { - context.ReportDiagnostic(DiagnosticDescriptors.FilterMultipleConsturtor, x.ApplicationSyntaxReference!.GetSyntax().GetLocation()); - return null!; - } + var filterType = x.AttributeClass!.TypeArguments[0]; + var filter = FilterInfo.Create(filterType); - return filter; + if (filter == null) + { + context.ReportDiagnostic(DiagnosticDescriptors.FilterMultipleConsturtor, x.ApplicationSyntaxReference!.GetSyntax().GetLocation()); + return null!; } - return null!; + + return filter; }) .ToArray(); if (typeFilters.Any(x => x == null)) @@ -453,22 +450,19 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta } var methodFilters = methodSymbol.GetAttributes() + .Where(x => x.AttributeClass?.Name == "ConsoleAppFilterAttribute") .Select(x => { - if (x.AttributeClass?.Name == "ConsoleAppFilterAttribute") - { - var filterType = x.AttributeClass.TypeArguments[0]; - var filter = FilterInfo.Create(filterType); - - if (filter == null) - { - context.ReportDiagnostic(DiagnosticDescriptors.FilterMultipleConsturtor, x.ApplicationSyntaxReference!.GetSyntax().GetLocation()); - return null!; - } + var filterType = x.AttributeClass!.TypeArguments[0]; + var filter = FilterInfo.Create(filterType); - return filter; + if (filter == null) + { + context.ReportDiagnostic(DiagnosticDescriptors.FilterMultipleConsturtor, x.ApplicationSyntaxReference!.GetSyntax().GetLocation()); + return null!; } - return null!; + + return filter; }) .ToArray(); if (methodFilters.Any(x => x == null)) diff --git a/src/ConsoleAppFramework5/Properties/launchSettings.json b/src/ConsoleAppFramework/Properties/launchSettings.json similarity index 100% rename from src/ConsoleAppFramework5/Properties/launchSettings.json rename to src/ConsoleAppFramework/Properties/launchSettings.json diff --git a/src/ConsoleAppFramework5/RoslynExtensions.cs b/src/ConsoleAppFramework/RoslynExtensions.cs similarity index 100% rename from src/ConsoleAppFramework5/RoslynExtensions.cs rename to src/ConsoleAppFramework/RoslynExtensions.cs diff --git a/src/ConsoleAppFramework/SimpleConsoleLogger.cs b/src/ConsoleAppFramework/SimpleConsoleLogger.cs deleted file mode 100644 index 054d5e5..0000000 --- a/src/ConsoleAppFramework/SimpleConsoleLogger.cs +++ /dev/null @@ -1,114 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using System.Linq; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Logging; -using System; -using ConsoleAppFramework.Logging; - -namespace ConsoleAppFramework.Logging -{ - public class SimpleConsoleLoggerProvider : ILoggerProvider - { - readonly SimpleConsoleLogger loggerDefault; - readonly SimpleConsoleLogger loggerHostingInternal; - - public SimpleConsoleLoggerProvider() - { - loggerDefault = new SimpleConsoleLogger(LogLevel.Trace); - loggerHostingInternal = new SimpleConsoleLogger(LogLevel.Information); - } - - public ILogger CreateLogger(string categoryName) - { - // NOTE: It omits unimportant log messages from Microsoft.Extension.Hosting.Internal.* - return categoryName.StartsWith("Microsoft.Extensions.Hosting.Internal") - ? loggerHostingInternal - : loggerDefault; - } - - public void Dispose() - { - } - } - - public class SimpleConsoleLogger : ILogger - { - readonly LogLevel minimumLogLevel; - - public SimpleConsoleLogger(LogLevel minimumLogLevel) - { - this.minimumLogLevel = minimumLogLevel; - } - - public IDisposable BeginScope(TState state) - { - return NullDisposable.Instance; - } - - public bool IsEnabled(LogLevel logLevel) - { - return minimumLogLevel <= logLevel; - } - - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) - { - if (formatter == null) throw new ArgumentNullException(nameof(formatter)); - - if (minimumLogLevel > logLevel) return; - - var msg = formatter(state, exception); - - if (!string.IsNullOrEmpty(msg)) - { - Console.WriteLine(msg); - } - - if (exception != null) - { - Console.WriteLine(exception.ToString()); - } - } - - class NullDisposable : IDisposable - { - public static readonly IDisposable Instance = new NullDisposable(); - - public void Dispose() - { - } - } - } -} - -namespace ConsoleAppFramework -{ - public static class SimpleConsoleLoggerExtensions - { - /// - /// use ConsoleAppFramework.Logging.SimpleConsoleLogger. - /// - /// - /// - public static ILoggingBuilder AddSimpleConsole(this ILoggingBuilder builder) - { - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); - return builder; - } - - /// - /// Remove default ConsoleLoggerProvider and replace to SimpleConsoleLogger. - /// - public static ILoggingBuilder ReplaceToSimpleConsole(this ILoggingBuilder builder) - { - // Use SimpleConsoleLogger instead of the default ConsoleLogger. - var consoleLogger = builder.Services.FirstOrDefault(x => x.ImplementationType?.FullName == "Microsoft.Extensions.Logging.Console.ConsoleLoggerProvider"); - if (consoleLogger != null) - { - builder.Services.Remove(consoleLogger); - } - - builder.AddSimpleConsole(); - return builder; - } - } -} \ No newline at end of file diff --git a/src/ConsoleAppFramework5/SourceBuilder.cs b/src/ConsoleAppFramework/SourceBuilder.cs similarity index 100% rename from src/ConsoleAppFramework5/SourceBuilder.cs rename to src/ConsoleAppFramework/SourceBuilder.cs diff --git a/src/ConsoleAppFramework5/WellKnownTypes.cs b/src/ConsoleAppFramework/WellKnownTypes.cs similarity index 100% rename from src/ConsoleAppFramework5/WellKnownTypes.cs rename to src/ConsoleAppFramework/WellKnownTypes.cs diff --git a/src/ConsoleAppFramework/release.snk b/src/ConsoleAppFramework/release.snk deleted file mode 100644 index 79d35090adb68963e4445962961a7558c58b1641..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50097ru_O}0e_yW7IB2$6zcC*rP{qs}OuOJR zph)aC`Y4EU)gF!C*Ls8KHOA^WK*KTqiP5@|MxiRc)rj$w%|m?tfh8t1)RGje_OTtw z>3sZc4}pM!<-4!74v7x!8Bu&0k`o78DNSFcl~&C{7+^u6sb7r$IB8c~?M>p{k6pqq zR0E-*Am~_IE)R0o_GjdaDxE48gVRWCl;8TZZ!YPw&)3pHWu0-|kkq4}$#J+1NGu7b zc*wBkdTtzj*;D_=kNB~17?6^0ZEm5`l2(bvvs+;fyr35&zC36LW#;buczH^wvR>Tc z2vvM0eMV5FG;hj-TP$zZuvpitns|3!=yfjl?} z&D^!!j%V`&)9-$@$rA__9$GG+I5EcR`?ZTVE{-ZHF`l!#9Ppp&nFj;YQ}m?egjXLK z?U|d=xQv4INE!zfWCcJ;mRk?4<{8x`vx^0%!-)zPd_=I==ZH^ntg!5k-}oC2O)YtP z(+{$V_Ns!5I??OB4*G&xTo#@?Y~?sDYjqWnIMAgUC=*`WtQ4!6@+-jiO*7#iYN3!}h0>u*&P!Sb=CjOwCg{l~kT4 zTIE>2?@RJGK6G*{O@in@P>BYDuqU*$Zih8=Bz7=1P5H;a=Dc%G<>drYczFHE$QO_> in?4FsX;K^FBPqZ7?t2C?ril&js>@xUdTJ)exXwkfA0fE_ diff --git a/src/ConsoleAppFramework5/CommandHelpBuilder.cs b/src/ConsoleAppFramework5/CommandHelpBuilder.cs deleted file mode 100644 index cf4e72d..0000000 --- a/src/ConsoleAppFramework5/CommandHelpBuilder.cs +++ /dev/null @@ -1,363 +0,0 @@ -using Microsoft.CodeAnalysis; -using System.Text; - -namespace ConsoleAppFramework; - -public static class CommandHelpBuilder -{ - public static string BuildRootHelpMessage(Command command) - { - return BuildHelpMessageCore(command, showCommandName: false, showCommand: false); - } - - public static string BuildRootHelpMessage(Command[] commands) - { - var sb = new StringBuilder(); - - var rootCommand = commands.FirstOrDefault(x => x.IsRootCommand); - var withoutRoot = commands.Where(x => !x.IsRootCommand).ToArray(); - - if (rootCommand != null && withoutRoot.Length == 0) - { - return BuildRootHelpMessage(commands[0]); - } - - if (rootCommand != null) - { - sb.AppendLine(BuildHelpMessageCore(rootCommand, false, withoutRoot.Length != 0)); - } - else - { - sb.AppendLine("Usage: [command] [-h|--help] [--version]"); - sb.AppendLine(); - } - - if (withoutRoot.Length == 0) return sb.ToString(); - - var helpDefinitions = withoutRoot.OrderBy(x => x.CommandFullName).ToArray(); - - var list = BuildMethodListMessage(helpDefinitions, out _); - sb.Append(list); - - return sb.ToString(); - } - - public static string BuildCommandHelpMessage(Command command) - { - return BuildHelpMessageCore(command, showCommandName: command.CommandName != "", showCommand: false); - } - - static string BuildHelpMessageCore(Command command, bool showCommandName, bool showCommand) - { - var definition = CreateCommandHelpDefinition(command); - - var sb = new StringBuilder(); - - sb.AppendLine(BuildUsageMessage(definition, showCommandName, showCommand)); - - if (!string.IsNullOrEmpty(definition.Description)) - { - sb.AppendLine(); - sb.AppendLine(definition.Description); - } - - if (definition.Options.Any()) - { - var hasArgument = definition.Options.Any(x => x.Index.HasValue); - var hasOptions = definition.Options.Any(x => !x.Index.HasValue); - - if (hasArgument) - { - sb.AppendLine(); - sb.AppendLine(BuildArgumentsMessage(definition)); - } - - if (hasOptions) - { - sb.AppendLine(); - sb.AppendLine(BuildOptionsMessage(definition)); - } - } - - return sb.ToString(); - } - - static string BuildUsageMessage(CommandHelpDefinition definition, bool showCommandName, bool showCommand) - { - var sb = new StringBuilder(); - sb.Append($"Usage:"); - - if (showCommandName) - { - sb.Append($" {definition.CommandName}"); - } - - if (showCommand) - { - sb.Append(" [command]"); - } - - if (definition.Options.Any(x => x.Index.HasValue)) - { - sb.Append(" [arguments...]"); - } - - if (definition.Options.Any(x => !x.Index.HasValue)) - { - sb.Append(" [options...]"); - } - - sb.Append(" [-h|--help] [--version]"); - - return sb.ToString(); - } - - static string BuildArgumentsMessage(CommandHelpDefinition definition) - { - var argumentsFormatted = definition.Options - .Where(x => x.Index.HasValue) - .Select(x => (Argument: $"[{x.Index}] {x.FormattedValueTypeName}", x.Description)) - .ToArray(); - - if (!argumentsFormatted.Any()) return string.Empty; - - var maxWidth = argumentsFormatted.Max(x => x.Argument.Length); - - var sb = new StringBuilder(); - - sb.AppendLine("Arguments:"); - var first = true; - foreach (var arg in argumentsFormatted) - { - if (first) - { - first = false; - } - else - { - sb.AppendLine(); - } - var padding = maxWidth - arg.Argument.Length; - - sb.Append(" "); - sb.Append(arg.Argument); - if (!string.IsNullOrEmpty(arg.Description)) - { - for (var i = 0; i < padding; i++) - { - sb.Append(' '); - } - - sb.Append(" "); - sb.Append(arg.Description); - } - } - - return sb.ToString(); - } - - static string BuildOptionsMessage(CommandHelpDefinition definition) - { - var optionsFormatted = definition.Options - .Where(x => !x.Index.HasValue) - .Select(x => (Options: string.Join("|", x.Options) + (x.IsFlag ? string.Empty : $" {x.FormattedValueTypeName}{(x.IsParams ? "..." : "")}"), x.Description, x.IsRequired, x.IsFlag, x.DefaultValue)) - .ToArray(); - - if (!optionsFormatted.Any()) return string.Empty; - - var maxWidth = optionsFormatted.Max(x => x.Options.Length); - - var sb = new StringBuilder(); - - sb.AppendLine("Options:"); - var first = true; - foreach (var opt in optionsFormatted) - { - if (first) - { - first = false; - } - else - { - sb.AppendLine(); - } - - var options = opt.Options; - var padding = maxWidth - options.Length; - - sb.Append(" "); - sb.Append(options); - for (var i = 0; i < padding; i++) - { - sb.Append(' '); - } - - sb.Append(" "); - sb.Append(opt.Description); - - if (opt.IsFlag) - { - sb.Append($" (Optional)"); - } - else if (opt.DefaultValue != null) - { - sb.Append($" (Default: {opt.DefaultValue})"); - } - else if (opt.IsRequired) - { - sb.Append($" (Required)"); - } - } - - return sb.ToString(); - } - - static string BuildMethodListMessage(IEnumerable commands, out int maxWidth) - { - var formatted = commands - .Select(x => - { - var full = x.CommandName; - if (x.CommandPath.Length > 0) - { - full = string.Join(" ", x.CommandPath) + " " + x.CommandName; - } - - return (Command: full, x.Description); - }) - .ToArray(); - maxWidth = formatted.Max(x => x.Command.Length); - - var sb = new StringBuilder(); - - sb.AppendLine("Commands:"); - foreach (var item in formatted) - { - sb.Append(" "); - sb.Append(item.Command); - if (string.IsNullOrEmpty(item.Description)) - { - sb.AppendLine(); - } - else - { - var padding = maxWidth - item.Command.Length; - for (var i = 0; i < padding; i++) - { - sb.Append(' '); - } - - sb.Append(" "); - sb.AppendLine(item.Description); - } - } - - return sb.ToString(); - } - - static CommandHelpDefinition CreateCommandHelpDefinition(Command descriptor) - { - var parameterDefinitions = new List(); - - foreach (var item in descriptor.Parameters) - { - // ignore DI params. - if (!item.IsParsable) continue; - - // -i, -input | [default=foo]... - - var index = item.ArgumentIndex == -1 ? null : (int?)item.ArgumentIndex; - var options = new List(); - if (item.ArgumentIndex != -1) - { - options.Add($"[{item.ArgumentIndex}]"); - } - else - { - // aliases first - foreach (var alias in item.Aliases) - { - options.Add(alias); - } - options.Add("--" + item.Name); - } - - var description = item.Description; - var isFlag = item.Type.SpecialType == Microsoft.CodeAnalysis.SpecialType.System_Boolean; - var isParams = item.IsParams; - - var defaultValue = default(string); - if (item.HasDefaultValue) - { - defaultValue = item.DefaultValue == null ? "null" : item.DefaultValueToString(castValue: false, enumIncludeTypeName: false); - if (isFlag) - { - if (item.DefaultValue is true) - { - // bool option with true default value is not flag. - isFlag = false; - } - else if (item.DefaultValue is false) - { - // false default value should be omitted for flag. - defaultValue = null; - } - } - } - - var paramTypeName = item.ToTypeShortString(); - parameterDefinitions.Add(new CommandOptionHelpDefinition(options.Distinct().ToArray(), description, paramTypeName, defaultValue, index, isFlag, isParams)); - } - - var commandName = descriptor.CommandName; - if (descriptor.CommandPath.Length != 0) - { - commandName = string.Join(" ", descriptor.CommandPath) + " " + descriptor.CommandName; - } - - return new CommandHelpDefinition( - commandName, - parameterDefinitions.ToArray(), - descriptor.Description - ); - } - - class CommandHelpDefinition - { - public string CommandName { get; } - public CommandOptionHelpDefinition[] Options { get; } - public string Description { get; } - - public CommandHelpDefinition(string command, CommandOptionHelpDefinition[] options, string description) - { - CommandName = command; - Options = options; - Description = description; - } - } - - class CommandOptionHelpDefinition - { - public string[] Options { get; } - public string Description { get; } - public string? DefaultValue { get; } - public string ValueTypeName { get; } - public int? Index { get; } - - public bool IsRequired => DefaultValue == null && !IsParams; - public bool IsFlag { get; } - public bool IsParams { get; } - public string FormattedValueTypeName => "<" + ValueTypeName + ">"; - - public CommandOptionHelpDefinition(string[] options, string description, string valueTypeName, string? defaultValue, int? index, bool isFlag, bool isParams) - { - Options = options; - Description = description; - ValueTypeName = valueTypeName; - DefaultValue = defaultValue; - Index = index; - IsFlag = isFlag; - IsParams = isParams; - } - } -} \ No newline at end of file diff --git a/src/ConsoleAppFramework5/ConsoleAppFramework5.csproj b/src/ConsoleAppFramework5/ConsoleAppFramework5.csproj deleted file mode 100644 index b6c00c5..0000000 --- a/src/ConsoleAppFramework5/ConsoleAppFramework5.csproj +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - netstandard2.0 - 12 - enable - enable - ConsoleAppFramework - - true - cs - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - diff --git a/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppFramework.GeneratorTests.csproj b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppFramework.GeneratorTests.csproj index 961f197..6d6be79 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppFramework.GeneratorTests.csproj +++ b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppFramework.GeneratorTests.csproj @@ -21,7 +21,7 @@ - + diff --git a/tests/ConsoleAppFramework.Tests/AssemblyInfo.cs b/tests/ConsoleAppFramework.Tests/AssemblyInfo.cs deleted file mode 100644 index 7db8497..0000000 --- a/tests/ConsoleAppFramework.Tests/AssemblyInfo.cs +++ /dev/null @@ -1,3 +0,0 @@ -using Xunit; - -[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file diff --git a/tests/ConsoleAppFramework.Tests/ConsoleAppFramework.Tests.csproj b/tests/ConsoleAppFramework.Tests/ConsoleAppFramework.Tests.csproj deleted file mode 100644 index 42e1567..0000000 --- a/tests/ConsoleAppFramework.Tests/ConsoleAppFramework.Tests.csproj +++ /dev/null @@ -1,27 +0,0 @@ - - - - net6.0 - false - true - ..\..\src\ConsoleAppFramework\release.snk - - - - - - - - - - - - - - - - - - - - diff --git a/tests/ConsoleAppFramework.Tests/Integration/CaptureConsoleOutput.cs b/tests/ConsoleAppFramework.Tests/Integration/CaptureConsoleOutput.cs deleted file mode 100644 index 3816e8a..0000000 --- a/tests/ConsoleAppFramework.Tests/Integration/CaptureConsoleOutput.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.IO; - -namespace ConsoleAppFramework.Integration.Test -{ - public class CaptureConsoleOutput : IDisposable - { - private readonly TextWriter _originalWriter; - private readonly StringWriter _stringWriter; - - public CaptureConsoleOutput() - { - _originalWriter = Console.Out; - _stringWriter = new StringWriter(); - Console.SetOut(_stringWriter); - } - - public string Output => _stringWriter.ToString(); - - public void Dispose() - { - Console.SetOut(_originalWriter); - } - } -} \ No newline at end of file diff --git a/tests/ConsoleAppFramework.Tests/Integration/CommandFilterTest.cs b/tests/ConsoleAppFramework.Tests/Integration/CommandFilterTest.cs deleted file mode 100644 index 07ffc8b..0000000 --- a/tests/ConsoleAppFramework.Tests/Integration/CommandFilterTest.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FluentAssertions; -using Microsoft.Extensions.Hosting; -using Xunit; -// ReSharper disable UnusedMember.Local -// ReSharper disable ClassNeverInstantiated.Local - -namespace ConsoleAppFramework.Integration.Test; - -public class FilterTest -{ - [Fact] - public void ApplyAttributeFilterTest() - { - using var console = new CaptureConsoleOutput(); - var args = new[] { "test-argument-name" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("[in filter] before"); - console.Output.Should().Contain(args[0]); - console.Output.Should().Contain("[in filter] after"); - } - - /// - private class TestConsoleApp : ConsoleAppBase - { - [RootCommand] - [ConsoleAppFilter(typeof(TestFilter))] - public void RootCommand([Option(index: 0)] string someArgument) => Console.WriteLine(someArgument); - } - - /// - private class TestFilter : ConsoleAppFilter - { - /// - public override async ValueTask Invoke(ConsoleAppContext context, Func next) - { - Console.WriteLine("[in filter] before"); - await next(context); - Console.WriteLine("[in filter] after"); - } - } -} - diff --git a/tests/ConsoleAppFramework.Tests/Integration/HelpUsageTest.cs b/tests/ConsoleAppFramework.Tests/Integration/HelpUsageTest.cs deleted file mode 100644 index 773af0b..0000000 --- a/tests/ConsoleAppFramework.Tests/Integration/HelpUsageTest.cs +++ /dev/null @@ -1,37 +0,0 @@ -using ConsoleAppFramework.Integration.Test; -using FluentAssertions; -using Microsoft.Extensions.Hosting; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Xunit; - -namespace ConsoleAppFramework.Tests.Integration -{ - public class HelpUsageTest - { - [Fact] - public async Task ConfigureApplicationName() - { - using var console = new CaptureConsoleOutput(); - var args = new string[] { }; - await Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args, new ConsoleAppOptions - { - ApplicationName = "foo" - }); - - var output = console.Output; - - output.Should().Contain("Usage: foo"); - } - - - public class Runner : ConsoleAppBase - { - [Command("hello")] - public void Hello() => Console.WriteLine("Hello"); - } - } -} diff --git a/tests/ConsoleAppFramework.Tests/Integration/InterceptorTest.cs b/tests/ConsoleAppFramework.Tests/Integration/InterceptorTest.cs deleted file mode 100644 index e2d1d3a..0000000 --- a/tests/ConsoleAppFramework.Tests/Integration/InterceptorTest.cs +++ /dev/null @@ -1,105 +0,0 @@ -//using System; -//using System.Collections.Generic; -//using System.Threading.Tasks; -//using FluentAssertions; -//using Microsoft.Extensions.Hosting; -//using Microsoft.Extensions.Logging; -//using Xunit; - -//// ReSharper disable InconsistentNaming - -//namespace ConsoleAppFramework.Integration.Test -//{ -// public partial class InterceptorTest -// { -// [Fact] -// public void Single() -// { -// using var console = new CaptureConsoleOutput(); -// var args = new[] { "Cysharp" }; -// var interceptor = new TestInterceptor(); -// Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args, interceptor); - -// interceptor.Outputs.Should().Equal("OnEngineBeginAsync", "OnMethodBeginAsync", "OnMethodEndAsync", "OnEngineCompleteAsync"); -// console.Output.Should().Contain("Hello Cysharp"); -// } - -// //[Fact] -// //public void Single_Insufficient_Arguments() -// //{ -// // using var console = new CaptureConsoleOutput(); -// // var args = new string[] { }; -// // var interceptor = new TestInterceptor(); -// // Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args, interceptor); - -// // interceptor.Outputs.Should().Equal("OnEngineBeginAsync", "OnMethodBeginAsync", "OnMethodEndAsync", "OnEngineCompleteAsync"); -// // console.Output.Should().Contain("Usage"); -// //} - -// [Fact] -// public void Multi() -// { -// using var console = new CaptureConsoleOutput(); -// var args = new[] { "hello", "Cysharp" }; -// var interceptor = new TestInterceptor(); -// Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args, interceptor); - -// interceptor.Outputs.Should().Equal("OnEngineBeginAsync", "OnMethodBeginAsync", "OnMethodEndAsync", "OnEngineCompleteAsync"); -// console.Output.Should().Contain("Hello Cysharp"); -// } - -// [Fact] -// public void Multi_Insufficient_Arguments() -// { -// using var console = new CaptureConsoleOutput(); -// var args = new[] { "Cysharp" }; -// var interceptor = new TestInterceptor(); -// Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args, interceptor); - -// interceptor.Outputs.Should().Equal("OnEngineBeginAsync", "OnMethodBeginAsync", "OnMethodEndAsync", "OnEngineCompleteAsync"); -// console.Output.Should().Contain("Usage"); -// } - -// public class TestInterceptor : IConsoleAppInterceptor -// { -// public List Outputs { get; } = new List(); - -// public ValueTask OnEngineBeginAsync(IServiceProvider serviceProvider, ILogger logger) -// { -// Outputs.Add("OnEngineBeginAsync"); -// return default; -// } - -// public ValueTask OnMethodBeginAsync(ConsoleAppContext context) -// { -// Outputs.Add("OnMethodBeginAsync"); -// return default; -// } - -// public ValueTask OnMethodEndAsync(ConsoleAppContext context, string? errorMessageIfFailed, Exception? exceptionIfExists) -// { -// Outputs.Add("OnMethodEndAsync"); -// return default; -// } - -// public ValueTask OnEngineCompleteAsync(IServiceProvider serviceProvider, ILogger logger) -// { -// Outputs.Add("OnEngineCompleteAsync"); -// return default; -// } -// } - -// public class InterceptorTest_Single : ConsoleAppBase -// { -// public void Hello([Option(0)]string name) => Console.WriteLine($"Hello {name}"); -// } - -// public class InterceptorTest_Multi : ConsoleAppBase -// { -// [Command("hello")] -// public void Hello([Option(0)]string name) => Console.WriteLine($"Hello {name}"); -// [Command("hello2")] -// public void Hello2([Option(0)]string name) => Console.WriteLine($"Hello {name}"); -// } -// } -//} diff --git a/tests/ConsoleAppFramework.Tests/Integration/MultipleCommandTest.cs b/tests/ConsoleAppFramework.Tests/Integration/MultipleCommandTest.cs deleted file mode 100644 index 00ecff3..0000000 --- a/tests/ConsoleAppFramework.Tests/Integration/MultipleCommandTest.cs +++ /dev/null @@ -1,225 +0,0 @@ -using System; -using System.Threading.Tasks; -using FluentAssertions; -using Microsoft.Extensions.Hosting; -using Xunit; - -// ReSharper disable InconsistentNaming - -namespace ConsoleAppFramework.Integration.Test -{ - public partial class MultipleCommandTest - { - [Fact] - public async Task NoCommandAttribute() - { - using var console = new CaptureConsoleOutput(); - var args = new string[] { }; - (await Assert.ThrowsAsync(()=> Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args))) - .Message.Should().Contain("Found more than one root command."); - } - - public class CommandTests_Multiple_NoCommandAttribute : ConsoleAppBase - { - public void Hello() => Console.WriteLine("Hello"); - public void Konnichiwa() => Console.WriteLine("Konnichiwa"); - } - - [Fact] - public void Commands() - { - using var console = new CaptureConsoleOutput(); - var args = new string[] { }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Usage:"); - console.Output.Should().Contain("Commands:"); - console.Output.Should().Contain("hello"); - console.Output.Should().Contain("konnichiwa"); - } - - //[Fact] - //public void Commands_UnknownCommand() - //{ - // using var console = new CaptureConsoleOutput(); - // var args = new string[] { "unknown-command" }; - // Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - // console.Output.Should().Contain("Usage:"); - // console.Output.Should().Contain("Commands:"); - // console.Output.Should().Contain("hello"); - // console.Output.Should().Contain("konnichiwa"); - //} - - [Fact] - public void Commands_UnknownCommand_Help() - { - using var console = new CaptureConsoleOutput(); - var args = new string[] { "help", "-foo", "-bar" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Usage:"); - console.Output.Should().Contain("Commands:"); - console.Output.Should().Contain("hello"); - console.Output.Should().Contain("konnichiwa"); - } - - public class CommandTests_Multiple_Commands : ConsoleAppBase - { - [Command("hello")] - public void Hello() => Console.WriteLine("Hello"); - [Command("konnichiwa")] - public void Konnichiwa() => Console.WriteLine("Konnichiwa"); - } - - [Fact] - public void OptionAndArg() - { - using var console = new CaptureConsoleOutput(); - var args = new string[] { "hello", "Cysharp" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Hello Cysharp (18)"); - } - - [Fact] - public void OptionAndArg_Option() - { - using var console = new CaptureConsoleOutput(); - var args = new string[] { "hello", "Cysharp", "-age", "-128" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Hello Cysharp (-128)"); - } - - [Fact] - public void OptionAndArg_Option_ReverseOrdered() - { - using var console = new CaptureConsoleOutput(); - var args = new string[] { "hello", "-age", "-128", "Cysharp" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Hello Cysharp (-128)"); - } - - [Fact] - public void OptionAndArg_Help() - { - using var console = new CaptureConsoleOutput(); - var args = new string[] { "hello", "help" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - // console.Output.Should().Contain("Hello help (18)"); - } - - [Fact] - public void OptionAndArg_HelpAndOtherArgs() - { - using var console = new CaptureConsoleOutput(); - var args = new string[] { "hello", "--help", "-age", "-128" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - - console.Output.Should().Contain("Usage: hello"); - } - - [Fact] - public void OptionAndArg_HelpOptionLike() - { - using var console = new CaptureConsoleOutput(); - var args = new string[] { "hello", "-help" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Usage:"); - console.Output.Should().Contain("Arguments:"); - - // NOTE: Currently, ConsoleAppFramework treats the first argument as special. If the argument is '-help', it is same as '-help' option. - //console.Output.Should().Contain("Hello -help (-128)"); - } - - [Fact] - public void OptionAndArg_HelpOptionLikeAndOtherOptions() - { - using var console = new CaptureConsoleOutput(); - var args = new string[] { "hello", "--help", "-age", "-128" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - - console.Output.Should().Contain("Usage: hello"); - } - - [Fact] - public void CommandHelp_OptionAndArg() - { - using var console = new CaptureConsoleOutput(); - var args = new string[] { "hello", "--help" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Usage:"); - console.Output.Should().Contain("Arguments:"); - } - - public class CommandTests_Multiple_OptionAndArg : ConsoleAppBase - { - [Command("hello")] - public void Hello([Option(0)]string name, int age = 18) => Console.WriteLine($"Hello {name} ({age})"); - [Command("konnichiwa")] - public void Konnichiwa() => Console.WriteLine("Konnichiwa"); - } - - [Fact] - public void OptionAndArg_Option_MixedOrdered_Default() - { - using var console = new CaptureConsoleOutput(); - var args = new string[] { "hello", "-age", "18", "Hello", "Cysharp" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Hello Cysharp (18)"); - } - - [Fact] - public void OptionAndArg_Option_MixedOrdered_2() - { - using var console = new CaptureConsoleOutput(); - var args = new string[] { "hello", "Hello", "-age", "18", "Cysharp" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Hello Cysharp (18)"); - } - - [Fact] - public void OptionAndArg_Option_MixedOrdered_3() - { - using var console = new CaptureConsoleOutput(); - var args = new string[] { "hello", "Hello", "Cysharp", "-age", "18" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Hello Cysharp (18)"); - } - - [Fact] - public void OptionAndArg_Option_Mixed_Optional() - { - using var console = new CaptureConsoleOutput(); - var args = new string[] { "greet" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Konnichiwa Anonymous"); - } - - public class CommandTests_Multiple_OptionAndArg_MixedOrdered : ConsoleAppBase - { - [Command("hello")] - public void Hello([Option(1)]string name, int age, [Option(0)]string greeting) => Console.WriteLine($"{greeting} {name} ({age})"); - [Command("greet")] - public void Greet([Option(0)]string greeting = "Konnichiwa", [Option(0)]string name = "Anonymous") => Console.WriteLine($"{greeting} {name}"); - } - - [Fact] - public void OptionHelp() - { - using var console = new CaptureConsoleOutput(); - var args = new string[] { "--help" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Usage:"); - console.Output.Should().Contain("Commands:"); - console.Output.Should().Contain("hello"); - console.Output.Should().Contain("konnichiwa"); - } - - [Fact] - public void OptionVersion() - { - using var console = new CaptureConsoleOutput(); - var args = new string[] { "--version" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().MatchRegex(@"\d.\d.\d"); // NOTE: When running with unit test runner, it returns a version of the runner. - } - - } -} diff --git a/tests/ConsoleAppFramework.Tests/Integration/NamedSingleCommandTest.cs b/tests/ConsoleAppFramework.Tests/Integration/NamedSingleCommandTest.cs deleted file mode 100644 index 698f8ce..0000000 --- a/tests/ConsoleAppFramework.Tests/Integration/NamedSingleCommandTest.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System; -using FluentAssertions; -using Microsoft.Extensions.Hosting; -using Xunit; - -// ReSharper disable InconsistentNaming - -namespace ConsoleAppFramework.Integration.Test -{ - public partial class NamedSingleCommandTest - { - [Fact] - public void NamedCommand_NoArgs_CommandIsNotSpecified() - { - using var console = new CaptureConsoleOutput(); - var args = new string[] { }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Usage:"); - console.Output.Should().Contain("Commands:"); - } - - [Fact] - public void NamedCommand_NoArgs_Invoke() - { - using var console = new CaptureConsoleOutput(); - var args = new string[] { "hello" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Hello"); - } - - [Fact] - public void NamedCommand_NoArgs_CommandHelp() - { - using var console = new CaptureConsoleOutput(); - var args = new string[] { "help", "hello" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Usage:"); - console.Output.Should().Contain(" hello"); - } - - public class CommandTests_Single_Named_NoArgs : ConsoleAppBase - { - [Command("hello")] - public void Hello() => Console.WriteLine("Hello"); - } - - [Fact] - public void NamedCommand_OneArg_CommandIsNotSpecified() - { - using var console = new CaptureConsoleOutput(); - var args = new string[] { }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Usage:"); - console.Output.Should().Contain("Commands:"); - } - - [Fact] - public void NamedCommand_OneArg_Invoke() - { - using var console = new CaptureConsoleOutput(); - var args = new string[] { "hello", "Cysharp" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Hello Cysharp"); - } - - //[Fact] - //public void NamedCommand_OneArg_CommandHelp() - //{ - // using var console = new CaptureConsoleOutput(); - // var args = new string[] { "help", "hello" }; - // Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - // console.Output.Should().Contain("Usage:"); - // console.Output.Should().Contain("Arguments:"); - //} - - public class CommandTests_Single_Named_OneArg : ConsoleAppBase - { - [Command("hello")] - public void Hello([Option(0)]string name) => Console.WriteLine($"Hello {name}"); - } - - } -} diff --git a/tests/ConsoleAppFramework.Tests/Integration/SingleCommandTest.Arguments.cs b/tests/ConsoleAppFramework.Tests/Integration/SingleCommandTest.Arguments.cs deleted file mode 100644 index e955a02..0000000 --- a/tests/ConsoleAppFramework.Tests/Integration/SingleCommandTest.Arguments.cs +++ /dev/null @@ -1,154 +0,0 @@ -using System; -using FluentAssertions; -using Microsoft.Extensions.Hosting; -using Xunit; - -// ReSharper disable InconsistentNaming - -namespace ConsoleAppFramework.Integration.Test -{ - public partial class SingleCommandTest - { - [Fact] - public void NoOptions_OneRequiredArg() - { - using var console = new CaptureConsoleOutput(); - var args = new[] { "Cysharp" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Hello Cysharp"); - } - - [Fact] - public void NoOptions_OneRequiredArg_ArgHelp() - { - using var console = new CaptureConsoleOutput(); - var args = new[] { "help" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - - // can not execute #shoganai - // console.Output.Should().Contain("Hello help"); - } - - [Fact] - public void NoOptions_OneRequiredArg_Insufficient() - { - using var console = new CaptureConsoleOutput(); - var args = new string[] { }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Usage:"); - console.Output.Should().Contain("Arguments:"); - } - - [Fact] - public void NoOptions_OneRequiredArg_Help() - { - using var console = new CaptureConsoleOutput(); - var args = new[] { "-help" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Usage:"); - console.Output.Should().Contain("Arguments:"); - - // NOTE: Currently, ConsoleAppFramework treats the first argument as special. If the argument is '-help', it is same as '-help' option. - //console.Output.Should().Contain("Hello -version"); - } - - [Fact] - public void NoOptions_OneRequiredArg_Version() - { - using var console = new CaptureConsoleOutput(); - var args = new[] { "version" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().MatchRegex(@"\d.\d.\d"); // NOTE: When running with unit test runner, it returns a version of the runner. - - // NOTE: Currently, ConsoleAppFramework treats the first argument as special. If the argument is '-help', it is same as '-help' option. - //console.Output.Should().Contain("Hello -version"); - } - - public class CommandTests_Single_NoOptions_OneRequiredArg : ConsoleAppBase - { - public void Hello([Option(0)]string name) => Console.WriteLine($"Hello {name}"); - } - - [Fact] - public void NoOptions_OneOptionalArg() - { - using var console = new CaptureConsoleOutput(); - var args = new[] { "Cysharp" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Hello Cysharp"); - } - - [Fact] - public void NoOptions_OneOptionalArg_ArgHelp() - { - using var console = new CaptureConsoleOutput(); - var args = new[] { "help" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - - // can not execute #shoganai - // console.Output.Should().Contain("Hello help"); - } - - [Fact] - public void NoOptions_OneOptionalArg_NoInputArg() - { - using var console = new CaptureConsoleOutput(); - var args = new string[] { }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Hello Anonymous"); - } - - [Fact] - public void NoOptions_OneOptionalArg_Help() - { - using var console = new CaptureConsoleOutput(); - var args = new[] { "help" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Usage:"); - console.Output.Should().Contain("Arguments:"); - - // NOTE: Currently, ConsoleAppFramework treats the first argument as special. If the argument is '-help', it is same as '-help' option. - //console.Output.Should().Contain("Hello -help"); - } - - [Fact] - public void NoOptions_OneOptionalArg_Version() - { - using var console = new CaptureConsoleOutput(); - var args = new[] { "version" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().MatchRegex(@"\d.\d.\d"); // NOTE: When running with unit test runner, it returns a version of the runner. - - // NOTE: Currently, ConsoleAppFramework treats the first argument as special. If the argument is '-help', it is same as '-help' option. - //console.Output.Should().Contain("Hello -version"); - } - - public class CommandTests_Single_NoOptions_OneOptionalArgs : ConsoleAppBase - { - public void Hello([Option(0)]string name = "Anonymous") => Console.WriteLine($"Hello {name}"); - } - - [Fact] - public void CommandTests_Single_DateTimeOption_WithDoubleQuote() - { - using var console = new CaptureConsoleOutput(); - var args = new[] { "\"2022-07-01\"" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Trim().Should().Be(@"2022-07-01"); - } - - [Fact] - public void CommandTests_Single_DateTimeOption_WithoutDoubleQuote() - { - using var console = new CaptureConsoleOutput(); - var args = new[] { "2022-07-01" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Trim().Should().Be(@"2022-07-01"); - } - - public class CommandTests_Single_DateTimeOption: ConsoleAppBase - { - public void Hello([Option(0)]DateTime dt) => Console.WriteLine($"{dt:yyyy-MM-dd}"); - } - } -} diff --git a/tests/ConsoleAppFramework.Tests/Integration/SingleCommandTest.Options.cs b/tests/ConsoleAppFramework.Tests/Integration/SingleCommandTest.Options.cs deleted file mode 100644 index 430dbda..0000000 --- a/tests/ConsoleAppFramework.Tests/Integration/SingleCommandTest.Options.cs +++ /dev/null @@ -1,216 +0,0 @@ -using System; -using FluentAssertions; -using Microsoft.Extensions.Hosting; -using Xunit; - -// ReSharper disable InconsistentNaming - -namespace ConsoleAppFramework.Integration.Test -{ - public partial class SingleCommandTest - { - [Fact] - public void OneRequiredOption_NoArgs() - { - using var console = new CaptureConsoleOutput(); - var args = new[] { "-name", "Cysharp" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Hello Cysharp"); - } - - [Fact] - public void OneRequiredOption_NoArgs_OptionLikeValue() - { - using var console = new CaptureConsoleOutput(); - var args = new[] { "-name", "-help" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Hello -help"); - } - - [Fact] - public void OneRequiredOption_NoArgs_Insufficient() - { - using var console = new CaptureConsoleOutput(); - var args = new string[] { }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Usage:"); - console.Output.Should().Contain("Options:"); - } - - [Fact] - public void OneRequiredOption_NoArgs_Help() - { - using var console = new CaptureConsoleOutput(); - var args = new[] { "-help" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Usage:"); - console.Output.Should().Contain("Options:"); - } - - public class CommandTests_Single_OneRequiredOption_NoArgs : ConsoleAppBase - { - public void Hello(string name) => Console.WriteLine($"Hello {name}"); - } - - [Fact] - public void OneRequiredOneOptionalOptions_NoArgs_0() - { - using var console = new CaptureConsoleOutput(); - var args = new[] { "-name", "Cysharp" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Hello Cysharp (17)"); - } - - [Fact] - public void OneRequiredOneOptionalOptions_NoArgs_1() - { - using var console = new CaptureConsoleOutput(); - var args = new[] { "-name", "Cysharp", "-age", "256" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Hello Cysharp (256)"); - } - - [Fact] - public void OneRequiredOneOptionalOptions_NoArgs_OptionLikeValue() - { - using var console = new CaptureConsoleOutput(); - var args = new[] { "-name", "-help", "-age", "256" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Hello -help (256)"); - } - - [Fact] - public void OneRequiredOneOptionalOptions_NoArgs_Insufficient() - { - using var console = new CaptureConsoleOutput(); - var args = new string[] { }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Usage:"); - console.Output.Should().Contain("Options:"); - } - - [Fact] - public void OneRequiredOneOptionalOptions_NoArgs_Help() - { - using var console = new CaptureConsoleOutput(); - var args = new[] { "-help" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Usage:"); - console.Output.Should().Contain("Options:"); - } - - public class CommandTests_Single_OneRequiredOneOptionalOptions_NoArgs : ConsoleAppBase - { - public void Hello(string name, int age = 17) => Console.WriteLine($"Hello {name} ({age})"); - } - - - [Fact] - public void TwoOptionalOptions_NoArgs_0() - { - using var console = new CaptureConsoleOutput(); - var args = new[] { "-name", "Cysharp" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Hello Cysharp (17)"); - } - - [Fact] - public void TwoOptionalOptions_NoArgs_1() - { - using var console = new CaptureConsoleOutput(); - var args = new[] { "-name", "Cysharp", "-age", "256" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Hello Cysharp (256)"); - } - - [Fact] - public void TwoOptionalOptions_NoArgs_2() - { - using var console = new CaptureConsoleOutput(); - var args = new[] { "-age", "-256" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Hello Anonymous (-256)"); - } - - [Fact] - public void TwoOptionalOptions_NoArgs_Ambiguous() - { - using var console = new CaptureConsoleOutput(); - var args = new[] { "-name", "-help", "-age", "256" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Hello -help (256)"); - // console.GetOutputText().Should().Contain("Usage:"); - // console.GetOutputText().Should().Contain("Options:"); - } - - [Fact] - public void TwoOptionalOptions_NoArgs_Help() - { - using var console = new CaptureConsoleOutput(); - var args = new[] { "-help" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Usage:"); - console.Output.Should().Contain("Options:"); - } - - [Fact] - public void TwoOptionalOptions_NoArgs_AllDefaultValue() - { - using var console = new CaptureConsoleOutput(); - var args = new string[] { }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Hello Anonymous (17)"); - } - - public class CommandTests_Single_TwoOptionalOptions_NoArgs : ConsoleAppBase - { - public void Hello(string name = "Anonymous", int age = 17) => Console.WriteLine($"Hello {name} ({age})"); - } - - [Fact] - public void RequiredBoolAndOtherOption_NoArgs() - { - using var console = new CaptureConsoleOutput(); - var args = new string[] { "-hello", "-name", "Cysharp" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Hello Cysharp"); - } - - public class CommandTests_Single_RequiredBoolAndOtherOption_NoArgs : ConsoleAppBase - { - public void Hello(bool hello, string name) => Console.WriteLine($"{(hello ? "Hello" : "Konnichiwa")} {name}"); - } - - [Fact] - public void OptionalBoolAndRequiredOtherOption_NoArgs() - { - using var console = new CaptureConsoleOutput(); - var args = new string[] { "-name", "Cysharp" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Konnichiwa Cysharp"); - } - - [Fact] - public void OptionalBoolAndRequiredOtherOption_NoArgs_1() - { - using var console = new CaptureConsoleOutput(); - var args = new string[] { "-hello", "-name", "Cysharp" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Hello Cysharp"); - } - - [Fact] - public void Attempt_To_Call_Without_Parameter_Value() - { - using var console = new CaptureConsoleOutput(); - var args = new[] { "--name" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain(@"Value for parameter ""name"" is not provided."); - } - - public class CommandTests_Single_OptionalBoolAndRequiredOtherOption_NoArgs : ConsoleAppBase - { - public void Hello(string name, bool hello = false) => Console.WriteLine($"{(hello ? "Hello" : "Konnichiwa")} {name}"); - } - } -} diff --git a/tests/ConsoleAppFramework.Tests/Integration/SingleCommandTest.OptionsAndArguments.cs b/tests/ConsoleAppFramework.Tests/Integration/SingleCommandTest.OptionsAndArguments.cs deleted file mode 100644 index 822463c..0000000 --- a/tests/ConsoleAppFramework.Tests/Integration/SingleCommandTest.OptionsAndArguments.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System; -using FluentAssertions; -using Microsoft.Extensions.Hosting; -using Xunit; - -// ReSharper disable InconsistentNaming - -namespace ConsoleAppFramework.Integration.Test -{ - public partial class SingleCommandTest - { - [Fact] - public void OneRequiredOption_OneRequiredArg() - { - using var console = new CaptureConsoleOutput(); - var args = new[] { "Cysharp", "-age", "18" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Cysharp (18)"); - } - - [Fact] - public void OneRequiredOption_OneRequiredArg_OptionLikeValueArg() - { - using var console = new CaptureConsoleOutput(); - var args = new[] { "--C--", "-age", "18" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("--C-- (18)"); - } - - [Fact] - public void OneRequiredOption_OneRequiredArg_Insufficient() - { - using var console = new CaptureConsoleOutput(); - var args = new string[] { }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Usage:"); - console.Output.Should().Contain("Options:"); - } - - [Fact] - public void OneRequiredOption_OneRequiredArg_Insufficient_Options() - { - using var console = new CaptureConsoleOutput(); - var args = new string[] { "Cysharp" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Required parameter \"age\""); - } - - [Fact] - public void OneRequiredOption_OneRequiredArg_Help() - { - using var console = new CaptureConsoleOutput(); - var args = new[] { "-help" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Usage:"); - console.Output.Should().Contain("Options:"); - } - - public class CommandTests_OneRequiredOption_OneRequiredArg : ConsoleAppBase - { - public void Hello([Option(0)]string name, int age) => Console.WriteLine($"{name} ({age})"); - } - - [Fact] - public void OneOptionalOption_OneRequiredArg() - { - using var console = new CaptureConsoleOutput(); - var args = new[] { "Cysharp" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Cysharp (17)"); - } - - [Fact] - public void OneOptionalOption_OneRequiredArg_Option() - { - using var console = new CaptureConsoleOutput(); - var args = new[] { "Cysharp", "-age", "18" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Cysharp (18)"); - } - - [Fact] - public void OneOptionalOption_OneRequiredArg_Help() - { - using var console = new CaptureConsoleOutput(); - var args = new[] { "-help" }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("Usage:"); - console.Output.Should().Contain("Options:"); - } - - public class CommandTests_OneOptionalOption_OneRequiredArg : ConsoleAppBase - { - public void Hello([Option(0)]string name, int age = 17) => Console.WriteLine($"{name} ({age})"); - } - } -} diff --git a/tests/ConsoleAppFramework.Tests/Integration/SingleCommandTest.cs b/tests/ConsoleAppFramework.Tests/Integration/SingleCommandTest.cs deleted file mode 100644 index 4bc0e27..0000000 --- a/tests/ConsoleAppFramework.Tests/Integration/SingleCommandTest.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using FluentAssertions; -using Microsoft.Extensions.Hosting; -using Xunit; - -// ReSharper disable InconsistentNaming - -namespace ConsoleAppFramework.Integration.Test -{ - public partial class SingleCommandTest - { - [Fact] - public void NoOptions_NoArgs() - { - using var console = new CaptureConsoleOutput(); - var args = new string[] { }; - Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(args); - console.Output.Should().Contain("HelloMyWorld"); - } - - [Fact] - public void IntArguments() - { - using var console = new CaptureConsoleOutput(); - - var args = "--foo 1,2,3".Split(' '); - - ConsoleApp.RunAsync(args, (int[] foo) => - { - foreach (var item in foo) - { - Console.WriteLine(item); - } - }); - - console.Output.Should().Be(@"1 -2 -3 -"); - } - - [Fact] - public void StringArguments() - { - using var console = new CaptureConsoleOutput(); - - var args = "--foo a,b,c".Split(' '); - - ConsoleApp.RunAsync(args, (string[] foo) => - { - foreach (var item in foo) - { - Console.WriteLine(item); - } - }); - - console.Output.Should().Be(@"a -b -c -"); - } - - public class CommandTests_Single_NoOptions_NoArgs : ConsoleAppBase - { - public void Hello() => Console.WriteLine("HelloMyWorld"); - } - } -} diff --git a/tests/ConsoleAppFramework.Tests/Integration/ValidationAttributeTests.cs b/tests/ConsoleAppFramework.Tests/Integration/ValidationAttributeTests.cs deleted file mode 100644 index 692f323..0000000 --- a/tests/ConsoleAppFramework.Tests/Integration/ValidationAttributeTests.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using FluentAssertions; -using Xunit; - -// ReSharper disable UnusedMember.Global -// ReSharper disable UnusedParameter.Global - -namespace ConsoleAppFramework.Integration.Test; - -public class ValidationAttributeTests -{ - /// - /// Try to execute command with invalid option value. - /// - [Fact] - public void Validate_String_Length_Test() - { - using var console = new CaptureConsoleOutput(); - - const string optionName = "arg"; - const string optionValue = "too-large-string-value"; - - var args = new[] { nameof(AppWithValidationAttributes.StrLength), $"--{optionName}", optionValue }; - ConsoleApp.Run(args); - - // Validation should fail, so StrLength command should not be executed. - console.Output.Should().NotContain(AppWithValidationAttributes.Output); - - console.Output.Should().Contain(optionName); - console.Output.Should().Contain(optionValue); - } - - [Fact] - public void Command_With_Multiple_Params() - { - using var console = new CaptureConsoleOutput(); - - var args = new[] - { - nameof(AppWithValidationAttributes.MultipleParams), - "--second-arg", "10", - "--first-arg", "invalid-email-address" - }; - - ConsoleApp.Run(args); - - // Validation should fail, so StrLength command should not be executed. - console.Output.Should().NotContain(AppWithValidationAttributes.Output); - } - - /// - internal class AppWithValidationAttributes : ConsoleAppBase - { - public const string Output = $"hello from {nameof(AppWithValidationAttributes)}"; - - [Command(nameof(StrLength))] - public void StrLength([StringLength(maximumLength: 8)] string arg) => Console.WriteLine(Output); - - [Command(nameof(MultipleParams))] - public void MultipleParams( - [EmailAddress] string firstArg, - [Range(0, 2)] int secondArg) => Console.WriteLine(Output); - } -} \ No newline at end of file diff --git a/tests/ConsoleAppFramework.Tests/Legacy/CommandAttributeTest.cs b/tests/ConsoleAppFramework.Tests/Legacy/CommandAttributeTest.cs deleted file mode 100644 index 86d4949..0000000 --- a/tests/ConsoleAppFramework.Tests/Legacy/CommandAttributeTest.cs +++ /dev/null @@ -1,44 +0,0 @@ -using FluentAssertions; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using System.Threading.Tasks; -using Xunit; - -namespace ConsoleAppFramework.Tests -{ - public class CommandAttributeTest - { - class CommandAttributeTestCommand : ConsoleAppBase - { - ResultContainer _Result; - public CommandAttributeTestCommand(ResultContainer r) - { - _Result = r; - } - [Command("test")] - public void TestCommand(int value) - { - _Result.X = value; - } - } - class ResultContainer - { - public int X; - } - //[Fact] - //public async Task TestCommandName() - //{ - // var host = Host.CreateDefaultBuilder() - // .ConfigureServices((c, services) => - // { - // services.AddSingleton(); - // }) - // .UseConsoleAppFramework(new string[]{ "test", "-value", "1" }) - // .Build(); - // var result = host.Services.GetService(); - // await host.RunAsync(); - // result.X.Should().Be(1); - //} - - } -} \ No newline at end of file diff --git a/tests/ConsoleAppFramework.Tests/Legacy/CommandHelpTest.cs b/tests/ConsoleAppFramework.Tests/Legacy/CommandHelpTest.cs deleted file mode 100644 index d3e2ea1..0000000 --- a/tests/ConsoleAppFramework.Tests/Legacy/CommandHelpTest.cs +++ /dev/null @@ -1,337 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using FluentAssertions; -using FluentAssertions.Common; -using Microsoft.Extensions.DependencyInjection; -using Xunit; - -namespace ConsoleAppFramework.Tests -{ - public class CommandHelpTest - { - private CommandHelpBuilder CreateCommandHelpBuilder() => new CommandHelpBuilder(() => "Nantoka", null, new ConsoleAppOptions() { NameConverter = x => x.ToLower() }); - private CommandHelpBuilder CreateCommandHelpBuilder2() => new CommandHelpBuilder(() => "Nantoka", null, new ConsoleAppOptions() { NameConverter = x => x.ToLower() }); - private CommandHelpBuilder CreateCommandHelpBuilder3() => new CommandHelpBuilder(() => "Nantoka", null, new ConsoleAppOptions() { NameConverter = x => x.ToLower() }); - - [Fact] - public void BuildMethodListMessage() - { - var builder = CreateCommandHelpBuilder(); - var expected = @$" -Commands: - list-message-batch hello - list-message-batch YetAnotherHello - list-message-batch HelloWithAliasWithDescription Description of command -".TrimStart(); - - var app = ConsoleApp.CreateBuilder(new string[0]).Build(); - app.AddSubCommands(); - var descs = app.Host.Services.GetRequiredService().CommandDescriptors.GetSubCommands("list-message-batch"); - - var msg = builder.BuildMethodListMessage(descs, false, out _); - msg.Should().Be(expected); - } - - [Fact] - public void BuildUsageMessage_Types() - { - var builder = CreateCommandHelpBuilder(); - var expected = @"Usage: Nantoka - -Commands: - list-message-batch hello - list-message-batch HelloWithAliasWithDescription Description of command - list-message-batch YetAnotherHello -"; - - var app = ConsoleApp.CreateBuilder(new string[0]).Build(); - app.AddSubCommands(); - var descs = app.Host.Services.GetRequiredService().CommandDescriptors.GetSubCommands("list-message-batch"); - - builder.BuildHelpMessage(null, descs, false).Should().Be(expected); - } - - [Fact] - public void BuildUsageMessage_Type() - { - var app = ConsoleApp.CreateBuilder(new string[0]).Build(); - app.AddSubCommand("commandhelptestbatch", "Complex2", new CommandHelpTestBatch().Complex); - app.Host.Services.GetRequiredService().CommandDescriptors.TryGetDescriptor(new[] { "commandhelptestbatch", "Complex2" }, out var desc, out _); - - var builder = CreateCommandHelpBuilder(); - var def = builder.CreateCommandHelpDefinition(desc, false); - var expected = @"Usage: Nantoka commandhelptestbatch Complex2 <1st> <2nd> <3rd> [options...]"; - - builder.BuildUsageMessage(def, showCommandName: true, fromMultiCommand: true).Should().Be(expected); - } - - [Fact] - public void BuildUsageMessage_Single() - { - var app = ConsoleApp.CreateBuilder(new string[0]).Build(); - app.AddSubCommand("commandhelptestbatch", "Complex2", new CommandHelpTestBatch().Complex); - app.Host.Services.GetRequiredService().CommandDescriptors.TryGetDescriptor(new[] { "commandhelptestbatch", "Complex2" }, out var desc, out _); - - var builder = CreateCommandHelpBuilder(); - var def = builder.CreateCommandHelpDefinition(desc, true); - var expected = @"Usage: Nantoka <1st> <2nd> <3rd> [options...]"; - - builder.BuildUsageMessage(def, showCommandName: false, fromMultiCommand: false).Should().Be(expected); - } - - //[Fact] - //public void BuildUsageMessage_Single_IndexedOptionsOnly() - //{ - // var builder = CreateCommandHelpBuilder(); - // var def = builder.CreateCommandHelpDefinition(typeof(CommandHelpTestBatch).GetMethod(nameof(CommandHelpTestBatch.ComplexIndexedOnly))); - // var expected = @"Usage: Nantoka <1st> <2nd> <3rd>"; - - // builder.BuildUsageMessage(def, showCommandName: false, fromMultiCommand: false).Should().Be(expected); - //} - - - // [Fact] - // public void CreateCommandHelp_Single_NoDescription() - // { - // var builder = CreateCommandHelpBuilder(); - // var def = builder.CreateCommandHelpDefinition(typeof(CommandHelpTestBatch).GetMethod(nameof(CommandHelpTestBatch.ComplexIndexedOnlyNoDescription))); - // var expected = @" - //Usage: Nantoka <1st> <2nd> <3rd> - - //Arguments: - // [0] 1st - // [1] 2nd - // [2] 3rd - - //".TrimStart(); - - // builder.BuildHelpMessage(def, showCommandName: false, fromMultiCommand: false).Should().Be(expected); - // } - - // [Fact] - // public void CreateCommandHelp_Single_IndexedOptionsOnly() - // { - // var builder = CreateCommandHelpBuilder(); - // var def = builder.CreateCommandHelpDefinition(typeof(CommandHelpTestBatch).GetMethod(nameof(CommandHelpTestBatch.ComplexIndexedOnly))); - // var expected = @" - //Usage: Nantoka <1st> <2nd> <3rd> - - //Description of complex command2 - - //Arguments: - // [0] 1st - // [1] 2nd - // [2] 3rd - - //".TrimStart(); - - // builder.BuildHelpMessage(def, showCommandName: false, fromMultiCommand: false).Should().Be(expected); - // } - - // [Fact] - // public void CreateCommandHelp_Single() - // { - // { - // var builder = CreateCommandHelpBuilder(); - // var def = builder.CreateCommandHelpDefinition(typeof(CommandHelpTestBatch).GetMethod(nameof(CommandHelpTestBatch.Complex))); - // var expected = @" - //Usage: Nantoka <1st> <2nd> <3rd> [options...] - - //Description of complex command - - //Arguments: - // [0] 1st - // [1] 2nd - // [2] 3rd - - //Options: - // -anonArg0 (Required) - // -optA, -shortNameArg0 Option has short name (Required) - - //".TrimStart(); - - // builder.BuildHelpMessage(def, showCommandName: false, fromMultiCommand: false).Should().Be(expected); - // } - // { - // var builder = CreateCommandHelpBuilder2(); - // var def = builder.CreateCommandHelpDefinition(typeof(CommandHelpTestBatch).GetMethod(nameof(CommandHelpTestBatch.Complex))); - // var expected = @" - //Usage: Nantoka <1st> <2nd> <3rd> [options...] - - //Description of complex command - - //Arguments: - // [0] 1st - // [1] 2nd - // [2] 3rd - - //Options: - // --anonArg0 (Required) - // -optA, --shortNameArg0 Option has short name (Required) - - //".TrimStart(); - - // builder.BuildHelpMessage(def, showCommandName: false, fromMultiCommand: false).Should().Be(expected); - // } - // } - - // [Fact] - // public void CreateCommandHelp_RequiredOrNotRequired() - // { - // var builder = CreateCommandHelpBuilder(); - // var def = builder.CreateCommandHelpDefinition(typeof(CommandHelpTestBatch).GetMethod(nameof(CommandHelpTestBatch.OptionRequiredAndNotRequired))); - // var expected = @" - //Options: - // -foo desc1 (Required) - // -bar desc2 (Default: 999) - - //".TrimStart(); - - // builder.BuildOptionsMessage(def).Should().Be(expected); - // } - - // [Fact] - // public void CreateCommandHelp_BooleanWithoutDefault_ShownWithoutValue() - // { - // var builder = CreateCommandHelpBuilder(); - // var def = builder.CreateCommandHelpDefinition(typeof(CommandHelpTestBatch).GetMethod(nameof(CommandHelpTestBatch.OptionBooleanSwitchWithoutDefault))); - // var expected = @" - //Options: - // -f, -flag desc (Optional) - - //".TrimStart(); - - // builder.BuildOptionsMessage(def).Should().Be(expected); - // } - - // [Fact] - // public void CreateCommandHelp_BooleanWithTrueDefault_ShownWithValue() - // { - // var builder = CreateCommandHelpBuilder(); - // var def = builder.CreateCommandHelpDefinition(typeof(CommandHelpTestBatch).GetMethod(nameof(CommandHelpTestBatch.OptionBooleanSwitchWithTrueDefault))); - // var expected = @" - //Options: - // -f, -flag desc (Default: True) - - //".TrimStart(); - - // builder.BuildOptionsMessage(def).Should().Be(expected); - // } - - // [Fact] - // public void CreateCommandHelp_BooleanWithTrueDefault_ShownWithoutValue() - // { - // var builder = CreateCommandHelpBuilder(); - // var def = builder.CreateCommandHelpDefinition(typeof(CommandHelpTestBatch).GetMethod(nameof(CommandHelpTestBatch.OptionBooleanSwitchWithFalseDefault))); - // var expected = @" - //Options: - // -f, -flag desc (Optional) - - //".TrimStart(); - - // builder.BuildOptionsMessage(def).Should().Be(expected); - // } - } - - [Command("list-message-batch")] - public class CommandHelpTestListMessageBatch : ConsoleAppBase - { - public void Hello() - { - } - - [Command("YetAnotherHello")] - public void HelloWithAlias() - { - } - - [Command("HelloWithAliasWithDescription", "Description of command")] - public void HelloWithAliasWithDescription() - { - } - } - - public class CommandHelpTestBatch : ConsoleAppBase - { - public void Hello() - { - } - - [Command("YetAnotherHello")] - public void HelloWithAlias() - { - } - - [Command("HelloWithAliasWithDescription", "Description of command")] - public void HelloWithAliasWithDescription() - { - } - - [Command(new[] { "YetAnotherHello2", "HokanoHello" })] - public void HelloWithAliases() - { - } - - public void OptionalParameters([Option("x")] int xxx, [Option("y", "Option y")] int yyy) - { - } - - public void OptionalParametersSameShortName([Option("xxx")] int xxx, [Option("yyy", "Option y")] int yyy) - { - } - - public void OptionDefaultValue(int nano = 999) - { - } - - public void OptionRequiredAndNotRequired([Option("foo", "desc1")] string foo, [Option("bar", "desc2")] int bar = 999) - { - } - - public void OptionIndex([Option(2, "3rd")] int arg0, [Option(1, "2nd")] string arg1, [Option(0, "1st")] bool arg2) - { - } - - public void OptionBooleanSwitchWithoutDefault([Option("f", "desc")] bool flag) - { - } - - public void OptionBooleanSwitchWithTrueDefault([Option("f", "desc")] bool flag = true) - { - } - - public void OptionBooleanSwitchWithFalseDefault([Option("f", "desc")] bool flag = false) - { - } - - [Command(new[] { "Complex2", "cpx" }, "Description of complex command")] - public void Complex( - int anonArg0, - [Option("optA", "Option has short name")] - string shortNameArg0, - [Option(2, "3rd")] int arg0, - [Option(1, "2nd")] string arg1, - [Option(0, "1st")] bool arg2 - ) - { - } - - [Command("ComplexIndexedOnly", "Description of complex command2")] - public void ComplexIndexedOnly( - [Option(2, "3rd")] int arg0, - [Option(1, "2nd")] string arg1, - [Option(0, "1st")] bool arg2 - ) - { - } - - public void ComplexIndexedOnlyNoDescription( - [Option(2, "3rd")] int arg0, - [Option(1, "2nd")] string arg1, - [Option(0, "1st")] bool arg2 - ) - { - } - } -} diff --git a/tests/ConsoleAppFramework.Tests/Legacy/ExitCodeTest.cs b/tests/ConsoleAppFramework.Tests/Legacy/ExitCodeTest.cs deleted file mode 100644 index 1744003..0000000 --- a/tests/ConsoleAppFramework.Tests/Legacy/ExitCodeTest.cs +++ /dev/null @@ -1,111 +0,0 @@ -using Microsoft.Extensions.Hosting; -using System; -using System.Threading.Tasks; -using Xunit; - -namespace ConsoleAppFramework.Tests -{ - public class ExitCodeTest - { - public class ExitCodeTestBatch : ConsoleAppBase - { - [Command(nameof(NoExitCode))] - public void NoExitCode() - { - } - - [Command(nameof(NoExitCodeException))] - public void NoExitCodeException() - { - throw new Exception(); - } - - [Command(nameof(NoExitCodeWithTask))] - public Task NoExitCodeWithTask() - { - return Task.CompletedTask; - } - - [Command(nameof(ExitCode))] - public int ExitCode() - { - return 12345; - } - - [Command(nameof(ExitCodeException))] - public int ExitCodeException() - { - throw new Exception(); - } - - [Command(nameof(ExitCodeWithTask))] - public Task ExitCodeWithTask() - { - return Task.FromResult(54321); - } - - [Command(nameof(ExitCodeWithTaskException))] - public Task ExitCodeWithTaskException() - { - throw new Exception(); - } - } - - [Fact] - public async Task NoExitCode() - { - Environment.ExitCode = 0; - await new HostBuilder().RunConsoleAppFrameworkAsync(new[] { nameof(NoExitCode) }); - Assert.Equal(0, Environment.ExitCode); - } - - [Fact] - public async Task NoExitCodeWithTask() - { - Environment.ExitCode = 0; - await new HostBuilder().RunConsoleAppFrameworkAsync(new[] { nameof(NoExitCodeWithTask) }); - Assert.Equal(0, Environment.ExitCode); - } - - [Fact] - public async Task NoExitCodeException() - { - Environment.ExitCode = 0; - await new HostBuilder().RunConsoleAppFrameworkAsync(new[] { nameof(NoExitCodeException) }); - Assert.Equal(1, Environment.ExitCode); - } - - [Fact] - public async Task ExitCode() - { - Environment.ExitCode = 0; - await new HostBuilder().RunConsoleAppFrameworkAsync(new[] { nameof(ExitCode) }); - Assert.Equal(12345, Environment.ExitCode); - } - - [Fact] - public async Task ExitCodeException() - { - Environment.ExitCode = 0; - await new HostBuilder().RunConsoleAppFrameworkAsync(new[] { nameof(ExitCodeException) }); - Assert.Equal(1, Environment.ExitCode); - } - - [Fact] - public async Task ExitCodeWithTask() - { - Environment.ExitCode = 0; - await new HostBuilder().RunConsoleAppFrameworkAsync(new[] { nameof(ExitCodeWithTask) }); - Assert.Equal(54321, Environment.ExitCode); - } - - [Fact] - public async Task ExitCodeWithTaskException() - { - Environment.ExitCode = 0; - await new HostBuilder().RunConsoleAppFrameworkAsync(new[] { nameof(ExitCodeWithTaskException) }); - Assert.Equal(1, Environment.ExitCode); - } - - } -} diff --git a/tests/ConsoleAppFramework.Tests/Legacy/MultiContainedTest.cs b/tests/ConsoleAppFramework.Tests/Legacy/MultiContainedTest.cs deleted file mode 100644 index 31ddf81..0000000 --- a/tests/ConsoleAppFramework.Tests/Legacy/MultiContainedTest.cs +++ /dev/null @@ -1,81 +0,0 @@ -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using System.Threading.Tasks; -using Xunit; -using Xunit.Abstractions; - -namespace ConsoleAppFramework.Tests -{ - public class Multi1 : ConsoleAppBase - { - public void Hello1() - { - Context.Logger.LogInformation("ok"); - } - - public void Hello2(string input) - { - Context.Logger.LogInformation(input); - } - } - - public class Multi2 : ConsoleAppBase - { - public void Hello1([Option("x")]int xxx, [Option("y")]int yyy) - { - Context.Logger.LogInformation($"{xxx}:{yyy}"); - } - - public void Hello2(bool x, bool y, string foo, int nano = 999) - { - Context.Logger.LogInformation($"{x}:{y}:{foo}:{nano}"); - } - } - - public class MultiContainedTest - { - ITestOutputHelper testOutput; - - public MultiContainedTest(ITestOutputHelper testOutput) - { - this.testOutput = testOutput; - } - - [Fact] - public async Task MultiContained() - { - { - var args = "Multi1.Hello1".Split(' '); - var log = new LogStack(); - await new HostBuilder() - .ConfigureTestLogging(testOutput, log, true) - .RunConsoleAppFrameworkAsync(args); - log.InfoLogShouldBe(0, "ok"); - } - { - var args = "Multi1.Hello2 -input yeah".Split(' '); - var log = new LogStack(); - await new HostBuilder() - .ConfigureTestLogging(testOutput, log, true) - .RunConsoleAppFrameworkAsync(args); - log.InfoLogShouldBe(0, "yeah"); - } - { - var args = "Multi2.Hello1 -x 20 -y 30".Split(' '); - var log = new LogStack(); - await new HostBuilder() - .ConfigureTestLogging(testOutput, log, true) - .RunConsoleAppFrameworkAsync(args); - log.InfoLogShouldBe(0, "20:30"); - } - { - var args = "Multi2.Hello2 -x -y -foo yeah".Split(' '); - var log = new LogStack(); - await new HostBuilder() - .ConfigureTestLogging(testOutput, log, true) - .RunConsoleAppFrameworkAsync(args); - log.InfoLogShouldBe(0, "True:True:yeah:999"); - } - } - } -} diff --git a/tests/ConsoleAppFramework.Tests/Legacy/ParameterCheckTest.cs b/tests/ConsoleAppFramework.Tests/Legacy/ParameterCheckTest.cs deleted file mode 100644 index 909a38d..0000000 --- a/tests/ConsoleAppFramework.Tests/Legacy/ParameterCheckTest.cs +++ /dev/null @@ -1,46 +0,0 @@ -using FluentAssertions; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Xunit; -using Xunit.Abstractions; - -namespace ConsoleAppFramework.Tests -{ - public class ParameterCheckTest - { - ITestOutputHelper testOutput; - - public ParameterCheckTest(ITestOutputHelper testOutput) - { - this.testOutput = testOutput; - } - - public class DictionaryCheck : ConsoleAppBase - { - public void Hello(Dictionary q) - { - - foreach (var item in q) - { - Context.Logger.LogInformation($"{item.Key}:{item.Value}"); - } - } - } - - [Fact] - public async Task DictParse() - { - var args = @"-q {""Key1"":""Value1*""}".Split(' '); - - var log = new LogStack(); - await new HostBuilder() - .ConfigureTestLogging(testOutput, log, true) - .RunConsoleAppFrameworkAsync(args); - - log.InfoLogShouldBe(0, "Key1:Value1*"); - } - } -} diff --git a/tests/ConsoleAppFramework.Tests/Legacy/SingleContainedTest.cs b/tests/ConsoleAppFramework.Tests/Legacy/SingleContainedTest.cs deleted file mode 100644 index a3c60ba..0000000 --- a/tests/ConsoleAppFramework.Tests/Legacy/SingleContainedTest.cs +++ /dev/null @@ -1,256 +0,0 @@ -using FluentAssertions; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using System; -using System.Threading.Tasks; -using Xunit; -using Xunit.Abstractions; - -namespace ConsoleAppFramework.Tests -{ - public class SingleContainedTest - { - ITestOutputHelper testOutput; - - public SingleContainedTest(ITestOutputHelper testOutput) - { - this.testOutput = testOutput; - } - - public class SimpleZeroArgs : ConsoleAppBase - { - public void Hello() - { - Context.Logger.LogInformation($"ok"); - } - } - - [Fact] - public async Task SimpleZeroArgsTest() - { - var log = new LogStack(); - await new HostBuilder() - .ConfigureTestLogging(testOutput, log, true) - .RunConsoleAppFrameworkAsync(new string[0]); - log.InfoLogShouldBe(0, "ok"); - } - - public class SimpleTwoArgs : ConsoleAppBase - { - public void Hello( - string name, - int repeat) - { - Context.Logger.LogInformation($"name:{name}"); - Context.Logger.LogInformation($"repeat:{repeat}"); - } - } - - [Fact] - public async Task SimpleTwoArgsTest() - { - { - var args = "-name foo -repeat 3".Split(' '); - var log = new LogStack(); - await new HostBuilder() - .ConfigureTestLogging(testOutput, log, true) - .RunConsoleAppFrameworkAsync(args); - log.InfoLogShouldBe(0, "name:foo"); - log.InfoLogShouldBe(1, "repeat:3"); - } - { - //var args = "-repeat 3".Split(' '); - //var log = new LogStack(); - //using (TextWriterBridge.BeginSetConsoleOut(testOutput, log)) - //{ - // { - // await new HostBuilder() - // .ConfigureTestLogging(testOutput, log, true) - // .RunConsoleAppFrameworkAsync(args); - // } - - // log.ToStringInfo().Should().Contain("Required parameter \"name\" not found in argument"); - //} - - } - { - var log = new LogStack(); - using (TextWriterBridge.BeginSetConsoleOut(testOutput, log)) - { - var args = new string[0]; - await new HostBuilder().RunConsoleAppFrameworkAsync(args); - log.ToStringInfo().Should().Contain("Options:"); // ok to show help - } - } - } - - public class SimpleComplexArgs : ConsoleAppBase - { - public void Hello( - ComplexStructure person, - int repeat) - { - Context.Logger.LogInformation($"person.Age:{person.Age} person.Name:{person.Name}"); - Context.Logger.LogInformation($"repeat:{repeat}"); - } - } - - public class ComplexStructure - { - public int Age { get; set; } - public string Name { get; set; } - } - - [Fact] - public async Task SimpleComplexArgsTest() - { - { - var args = "-person {\"Age\":10,\"Name\":\"foo\"} -repeat 3".Split(' '); - var log = new LogStack(); - await new HostBuilder() - .ConfigureTestLogging(testOutput, log, true) - .RunConsoleAppFrameworkAsync(args); - log.InfoLogShouldBe(0, "person.Age:10 person.Name:foo"); - log.InfoLogShouldBe(1, "repeat:3"); - } - } - - public class TwoArgsWithOption : ConsoleAppBase - { - public void Hello( - [Option("n", "name of this")]string name, - [Option("r", "repeat msg")]int repeat) - { - Context.Logger.LogInformation($"name:{name}"); - Context.Logger.LogInformation($"repeat:{repeat}"); - } - } - - [Fact] - public async Task TwoArgsWithOptionTest() - { - { - var args = "-n foo -r 3".Split(' '); - var log = new LogStack(); - await new HostBuilder() - .ConfigureTestLogging(testOutput, log, true) - .RunConsoleAppFrameworkAsync(args); - log.InfoLogShouldBe(0, "name:foo"); - log.InfoLogShouldBe(1, "repeat:3"); - } - { - var log = new LogStack(); - using (TextWriterBridge.BeginSetConsoleOut(testOutput, log)) - { - var args = new string[0]; - await new HostBuilder().RunConsoleAppFrameworkAsync(args); - var strAssertion = log.ToStringInfo().Should(); - strAssertion.Contain("Options:"); // ok to show help - strAssertion.Contain("-n"); - strAssertion.Contain("name of this"); - strAssertion.Contain("-r"); - strAssertion.Contain("repeat msg"); - } - } - } - - public class TwoArgsWithDefault : ConsoleAppBase - { - public void Hello(string name, int repeat = 100, string hoo = null) - { - Context.Logger.LogInformation($"name:{name}"); - Context.Logger.LogInformation($"repeat:{repeat}"); - Context.Logger.LogInformation($"hoo:{hoo}"); - } - } - - [Fact] - public async Task TwoArgsWithDefaultTest() - { - { - var args = "-name foo".Split(' '); - var log = new LogStack(); - await new HostBuilder() - .ConfigureTestLogging(testOutput, log, true) - .RunConsoleAppFrameworkAsync(args); - log.InfoLogShouldBe(0, "name:foo"); - log.InfoLogShouldBe(1, "repeat:100"); - log.InfoLogShouldBe(2, "hoo:"); - } - } - - public class AllDefaultParameters : ConsoleAppBase - { - public void Hello(string name = "aaa", int repeat = 100, string hoo = null) - { - Context.Logger.LogInformation($"name:{name}"); - Context.Logger.LogInformation($"repeat:{repeat}"); - Context.Logger.LogInformation($"hoo:{hoo}"); - } - } - - [Fact] - public async Task AllDefaultParametersTest() - { - { - var args = new string[0]; - var log = new LogStack(); - await new HostBuilder() - .ConfigureTestLogging(testOutput, log, true) - .RunConsoleAppFrameworkAsync(args); - log.InfoLogShouldBe(0, "name:aaa"); - log.InfoLogShouldBe(1, "repeat:100"); - log.InfoLogShouldBe(2, "hoo:"); - } - } - - public class BooleanSwitch : ConsoleAppBase - { - public void Hello(string x, bool bar, bool foo = false, bool yeah = false) - { - Context.Logger.LogInformation($"x:{x}"); - Context.Logger.LogInformation($"bar:{bar}"); - Context.Logger.LogInformation($"foo:{foo}"); - Context.Logger.LogInformation($"yeah:{yeah}"); - } - } - - [Fact] - public async Task BooleanSwitchTest() - { - { - var log = new LogStack(); - var args = "-x foo -bar -foo -yeah".Split(' '); - await new HostBuilder() - .ConfigureTestLogging(testOutput, log, true) - .RunConsoleAppFrameworkAsync(args); - log.InfoLogShouldBe(0, "x:foo"); - log.InfoLogShouldBe(1, "bar:True"); - log.InfoLogShouldBe(2, "foo:True"); - log.InfoLogShouldBe(3, "yeah:True"); - } - { - var log = new LogStack(); - var args = "-x foo -bar -foo".Split(' '); - await new HostBuilder() - .ConfigureTestLogging(testOutput, log, true) - .RunConsoleAppFrameworkAsync(args); - log.InfoLogShouldBe(0, "x:foo"); - log.InfoLogShouldBe(1, "bar:True"); - log.InfoLogShouldBe(2, "foo:True"); - log.InfoLogShouldBe(3, "yeah:False"); - } - { - var log = new LogStack(); - var args = "-x foo -foo -yeah".Split(' '); - await new HostBuilder() - .ConfigureTestLogging(testOutput, log, true) - .RunConsoleAppFrameworkAsync(args); - log.InfoLogShouldBe(0, "x:foo"); - log.InfoLogShouldBe(1, "bar:False"); - log.InfoLogShouldBe(2, "foo:True"); - log.InfoLogShouldBe(3, "yeah:True"); - } - } - } -} diff --git a/tests/ConsoleAppFramework.Tests/Legacy/SubCommandTest.cs b/tests/ConsoleAppFramework.Tests/Legacy/SubCommandTest.cs deleted file mode 100644 index ccd69ac..0000000 --- a/tests/ConsoleAppFramework.Tests/Legacy/SubCommandTest.cs +++ /dev/null @@ -1,216 +0,0 @@ -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using System; -using System.Threading.Tasks; -using Xunit; -using Xunit.Abstractions; - -namespace ConsoleAppFramework.Tests -{ - - public class SubCommandTest - { - readonly ITestOutputHelper testOutput; - - public SubCommandTest(ITestOutputHelper testOutput) - { - this.testOutput = testOutput; - } - - public class TwoSubCommand : ConsoleAppBase - { - public void Main(double d) - { - Context.Logger.LogInformation($"d:{d}"); - } - - [Command("run")] - public void Run(string path, string pfx) - { - Context.Logger.LogInformation($"path:{path}"); - Context.Logger.LogInformation($"pfx:{pfx}"); - } - - [Command("sum")] - public void Sum([Option(0)]int x, [Option(1)]int y) - { - Context.Logger.LogInformation($"x:{x}"); - Context.Logger.LogInformation($"y:{y}"); - } - - [Command("opt")] - public void Option([Option(0)]string input, [Option("x")]int xxx, [Option("y")]int yyy) - { - Context.Logger.LogInformation($"input:{input}"); - Context.Logger.LogInformation($"x:{xxx}"); - Context.Logger.LogInformation($"y:{yyy}"); - } - } - - [Fact] - public async Task TwoSubCommandTest() - { - { - var args = "-d 12345.12345".Split(' '); - var log = new LogStack(); - await new HostBuilder() - .ConfigureTestLogging(testOutput, log, true) - .RunConsoleAppFrameworkAsync(args); - log.InfoLogShouldBe(0, "d:12345.12345"); - } - { - var args = "run -path foo -pfx bar".Split(' '); - var log = new LogStack(); - await new HostBuilder() - .ConfigureTestLogging(testOutput, log, true) - .RunConsoleAppFrameworkAsync(args); - log.InfoLogShouldBe(0, "path:foo"); - log.InfoLogShouldBe(1, "pfx:bar"); - } - { - var args = "sum 10 20".Split(' '); - var log = new LogStack(); - await new HostBuilder() - .ConfigureTestLogging(testOutput, log, true) - .RunConsoleAppFrameworkAsync(args); - log.InfoLogShouldBe(0, "x:10"); - log.InfoLogShouldBe(1, "y:20"); - } - { - var args = "opt foobarbaz -x 10 -y 20".Split(' '); - var log = new LogStack(); - await new HostBuilder() - .ConfigureTestLogging(testOutput, log, true) - .RunConsoleAppFrameworkAsync(args); - log.InfoLogShouldBe(0, "input:foobarbaz"); - log.InfoLogShouldBe(1, "x:10"); - log.InfoLogShouldBe(2, "y:20"); - } - } - - public class AliasCommand : ConsoleAppBase - { - [Command(new[] { "run", "r" })] - public void Run(string path, string pfx) - { - Context.Logger.LogInformation($"path:{path}"); - Context.Logger.LogInformation($"pfx:{pfx}"); - } - - [Command(new[] { "su", "summmm" })] - public void Sum([Option(0)]int x, [Option(1)]int y) - { - Context.Logger.LogInformation($"{x + y}"); - } - } - - [Fact] - public async Task AliasCommandTest() - { - { - var collection = new[]{ - "r -path foo -pfx bar".Split(' '), - "run -path foo -pfx bar".Split(' '), - }; - foreach (var args in collection) - { - var log = new LogStack(); - await new HostBuilder() - .ConfigureTestLogging(testOutput, log, true) - .RunConsoleAppFrameworkAsync(args); - log.InfoLogShouldBe(0, "path:foo"); - log.InfoLogShouldBe(1, "pfx:bar"); - } - } - { - { - var args = "su 10 20".Split(' '); - var log = new LogStack(); - await new HostBuilder() - .ConfigureTestLogging(testOutput, log, true) - .RunConsoleAppFrameworkAsync(args); - log.InfoLogShouldBe(0, "30"); - } - { - var args = "summmm 99 100".Split(' '); - var log = new LogStack(); - await new HostBuilder() - .ConfigureTestLogging(testOutput, log, true) - .RunConsoleAppFrameworkAsync(args); - log.InfoLogShouldBe(0, "199"); - } - } - } - - public class OverrideDefaultCommand : ConsoleAppBase - { - [Command("list")] - public void List() - { - Context.Logger.LogInformation($"lst"); - } - - [Command(new[] { "help", "h" })] - public void Help() - { - Context.Logger.LogInformation($"hlp"); - } - } - - [Fact] - public async Task OverrideDefaultCommandTest() - { - { - var args = "list".Split(' '); - var log = new LogStack(); - await new HostBuilder() - .ConfigureTestLogging(testOutput, log, true) - .RunConsoleAppFrameworkAsync(args); - log.InfoLogShouldBe(0, "lst"); - } - { - var args = "help".Split(' '); - var log = new LogStack(); - await new HostBuilder() - .ConfigureTestLogging(testOutput, log, true) - .RunConsoleAppFrameworkAsync(args); - log.InfoLogShouldBe(0, "hlp"); - } - { - var args = "h".Split(' '); - var log = new LogStack(); - await new HostBuilder() - .ConfigureTestLogging(testOutput, log, true) - .RunConsoleAppFrameworkAsync(args); - log.InfoLogShouldBe(0, "hlp"); - } - } - - public class NotFoundPath : ConsoleAppBase - { - [Command("run")] - public void Run(string path, string pfx, string thumbnail, string output, bool allowoverwrite = false) - { - Context.Logger.LogInformation($"path:{path}"); - Context.Logger.LogInformation($"pfx:{pfx}"); - Context.Logger.LogInformation($"thumbnail:{thumbnail}"); - Context.Logger.LogInformation($"output:{output}"); - Context.Logger.LogInformation($"allowoverwrite:{allowoverwrite}"); - } - } - - //[Fact] - //public async Task NotFoundPathTest() - //{ - // var args = "run -path -pfx test.pfx -thumbnail 123456 -output output.csproj -allowoverwrite".Split(' '); - // var log = new LogStack(); - - // await Assert.ThrowsAnyAsync(async () => - // { - // await new HostBuilder() - // .ConfigureTestLogging(testOutput, log, true) - // .RunConsoleAppFrameworkAsync(args); - // }); - //} - } -} diff --git a/tests/ConsoleAppFramework.Tests/XUnitLogger.cs b/tests/ConsoleAppFramework.Tests/XUnitLogger.cs deleted file mode 100644 index fc92f78..0000000 --- a/tests/ConsoleAppFramework.Tests/XUnitLogger.cs +++ /dev/null @@ -1,201 +0,0 @@ -using FluentAssertions; -using FluentAssertions.Primitives; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using Xunit.Abstractions; - -namespace ConsoleAppFramework.Tests -{ - public static class LoggingExtensions - { - public static IHostBuilder ConfigureTestLogging(this IHostBuilder builder, ITestOutputHelper testOutputHelper, LogStack logStack, bool throwExceptionOnError) - { - return builder - .ConfigureServices(x => x.AddSingleton(logStack)) - .ConfigureLogging(x => x.SetMinimumLevel(LogLevel.Trace).AddProvider(new XUnitLoggerProvider(testOutputHelper, logStack, throwExceptionOnError))); - } - } - - public class TextWriterBridge : TextWriter - { - readonly ITestOutputHelper helper; - readonly LogStack logStack; - - public TextWriterBridge(ITestOutputHelper helper, LogStack logStack) - { - this.helper = helper; - this.logStack = logStack; - } - - public override Encoding Encoding => Encoding.UTF8; - - public override void Write(string value) - { - logStack.Add(LogLevel.Information, value); - helper.WriteLine(value); - } - - public static IDisposable BeginSetConsoleOut(ITestOutputHelper helper, LogStack logStack) - { - var current = Console.Out; - Console.SetOut(new TextWriterBridge(helper, logStack)); - return new Scope(current); - } - - public struct Scope : IDisposable - { - TextWriter writer; - - public Scope(TextWriter writer) - { - this.writer = writer; - } - - public void Dispose() - { - Console.SetOut(writer); - } - } - } - - public class LogStack - { - readonly Dictionary> logs = new Dictionary>(6); - - public LogStack() - { - logs.Add(LogLevel.Trace, new List()); - logs.Add(LogLevel.Warning, new List()); - logs.Add(LogLevel.Information, new List()); - logs.Add(LogLevel.Debug, new List()); - logs.Add(LogLevel.Critical, new List()); - logs.Add(LogLevel.Error, new List()); - } - - public void Add(LogLevel level, string msg) - { - logs[level].Add(msg); - } - - public List InfoLog => logs[LogLevel.Information]; - public List ErrorLog => logs[LogLevel.Error]; - - public StringAssertions InfoLogShould(int index) => logs[LogLevel.Information][index].Should(); - public AndConstraint InfoLogShouldBe(int index, string expected) => InfoLogShould(index).Be(expected); - - public List GetLogs(LogLevel level) - { - return logs[level]; - } - - public void ClearAll() - { - foreach (var item in logs) - { - item.Value.Clear(); - } - } - - public string ToStringInfo() => ToString(LogLevel.Information); - - public string ToString(LogLevel level) - { - var sb = new StringBuilder(); - foreach (var item in GetLogs(level)) - { - sb.AppendLine(item); - } - return sb.ToString(); - } - } - - public class TestLogException : Exception - { - public TestLogException(string message) : base(message) - { - } - - public TestLogException(string message, Exception innerException) : base(message, innerException) - { - } - } - - public class XUnitLoggerProvider : ILoggerProvider - { - readonly XUnitLogger logger; - - public XUnitLoggerProvider(ITestOutputHelper testOutput, LogStack logStack, bool throwExceptionOnError) - { - logger = new XUnitLogger(testOutput, logStack, throwExceptionOnError); - } - - public ILogger CreateLogger(string categoryName) - { - return logger; - } - - public void Dispose() - { - } - } - - public class XUnitLogger : ILogger - { - readonly ITestOutputHelper testOutput; - readonly LogStack logStack; - readonly bool throwExceptionOnError; - - public XUnitLogger(ITestOutputHelper testOutput, LogStack logStack, bool throwExceptionOnError) - { - this.testOutput = testOutput; - this.logStack = logStack; - this.throwExceptionOnError = throwExceptionOnError; - } - - public IDisposable BeginScope(TState state) - { - return NullDisposable.Instance; - } - - public bool IsEnabled(LogLevel logLevel) - { - return true; - } - - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) - { - var msg = formatter(state, exception); - - if (logLevel == LogLevel.Error && throwExceptionOnError) - { - throw (exception != null ? new TestLogException(msg, exception) : new TestLogException(msg)); - } - - if (!string.IsNullOrEmpty(msg)) - { - logStack.Add(logLevel, msg); - testOutput.WriteLine(msg); - } - - if (exception != null) - { - logStack.Add(logLevel, exception.ToString()); - testOutput.WriteLine(exception.ToString()); - } - } - - class NullDisposable : IDisposable - { - public static readonly IDisposable Instance = new NullDisposable(); - - public void Dispose() - { - } - } - } -} From 8e937c6220789648860310cbad93618bb71b5e6a Mon Sep 17 00:00:00 2001 From: neuecc Date: Fri, 31 May 2024 08:52:33 +0900 Subject: [PATCH 41/54] r --- ReadMe.md | 143 +++++ .../CliFrameworkBenchmark.csproj | 1 + .../GeneratorSandbox/GeneratorSandbox.csproj | 1 + sandbox/GeneratorSandbox/Program.cs | 550 ++---------------- .../ConsoleAppGenerator.cs | 6 +- src/ConsoleAppFramework/Emitter.cs | 2 +- 6 files changed, 195 insertions(+), 508 deletions(-) diff --git a/ReadMe.md b/ReadMe.md index e815cee..8c09a21 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -2,6 +2,149 @@ ConsoleAppFramework === [![GitHub Actions](https://github.com/Cysharp/ConsoleAppFramework/workflows/Build-Debug/badge.svg)](https://github.com/Cysharp/ConsoleAppFramework/actions) [![Releases](https://img.shields.io/github/release/Cysharp/ConsoleAppFramework.svg)](https://github.com/Cysharp/ConsoleAppFramework/releases) +ConsoleAppFramework v5 is Zero Dependency, Zero Overhead, Zero Reflection, Zero Allocation, AOT Safe CLI Framework powered by C# Source Generator. Leveraging the latest features of .NET 8 and C# 12 ([IncrementalGenerator](https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md), [managed function pointer](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-9.0/function-pointers#function-pointers-1), [params arrays and default values lambda expression](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/lambda-expressions#input-parameters-of-a-lambda-expression), [`ISpanParsable`](https://learn.microsoft.com/en-us/dotnet/api/system.ispanparsable-1), [`PosixSignalRegistration`](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.posixsignalregistration), etc.), this library ensures maximum performance while maintaining flexibility and extensibility. + +![image](https://github.com/Cysharp/ConsoleAppFramework/assets/46207/db4bf599-9fe0-4ce4-801f-0003f44d5628) +> Set `RunStrategy=ColdStart WarmupCount=0` to calculate the cold start benchmark, which is suitable for CLI application. + +The magical performance is achieved by statically generating everything and parsing inline. Let's take a look at a minimal example: + +```csharp +using ConsoleAppFramework; + +// args: ./cmd --foo 10 --bar 20 +ConsoleApp.Run(args, (int foo, int bar) => Console.WriteLine($"Sum: {foo + bar}")); +``` + +Unlike typical Source Generators that use attributes as keys for generation, ConsoleAppFramework analyzes the provided lambda expressions or method references and generates the actual code body of the Run method. + +```csharp +namespace ConsoleAppFramework; + +internal static partial class ConsoleApp +{ + public static void Run(string[] args, Action command) + { + if (TryShowHelpOrVersion(args, 2, -1)) return; + + var arg0 = default(int); + var arg0Parsed = false; + var arg1 = default(int); + var arg1Parsed = false; + + try + { + for (int i = 0; i < args.Length; i++) + { + var name = args[i]; + + switch (name) + { + case "--foo": + { + if (!int.TryParse(args[++i], out arg0)) { ThrowArgumentParseFailed("foo", args[i]); } + arg0Parsed = true; + break; + } + case "--bar": + { + if (!int.TryParse(args[++i], out arg1)) { ThrowArgumentParseFailed("bar", args[i]); } + arg1Parsed = true; + break; + } + default: + if (string.Equals(name, "--foo", StringComparison.OrdinalIgnoreCase)) + { + if (!int.TryParse(args[++i], out arg0)) { ThrowArgumentParseFailed("foo", args[i]); } + arg0Parsed = true; + break; + } + if (string.Equals(name, "--bar", StringComparison.OrdinalIgnoreCase)) + { + if (!int.TryParse(args[++i], out arg1)) { ThrowArgumentParseFailed("bar", args[i]); } + arg1Parsed = true; + break; + } + ThrowArgumentNameNotFound(name); + break; + } + } + if (!arg0Parsed) ThrowRequiredArgumentNotParsed("foo"); + if (!arg1Parsed) ThrowRequiredArgumentNotParsed("bar"); + + command(arg0!, arg1!); + } + catch (Exception ex) + { + Environment.ExitCode = 1; + if (ex is ValidationException) + { + LogError(ex.Message); + } + else + { + LogError(ex.ToString()); + } + } + } + + static partial void ShowHelp(int helpId) + { + Log(""" +Usage: [options...] [-h|--help] [--version] + +Options: + --foo (Required) + --bar (Required) +"""); + } +} +``` + +As you can see, the code is straightforward and simple, making it easy to imagine the execution cost of the framework portion. That's right, it's zero. This technique was influenced by Rust's macros. Rust has [Attribute-like macros and Function-like macros](https://doc.rust-lang.org/book/ch19-06-macros.html), and ConsoleAppFramework's generation can be considered as Function-like macros. + + + + + + + + + +Dependency Injection, +async/await +Exit code, + +SIGINT/SIGTERM(Ctrl+C) handling with gracefully shutdown via CancellationToken, + +filter(middleware) pipeline, + +multi commands, +nested command, +options aliases, +params array, + +JSON argument, +help builder + + + + +Requirements + +.NET 8, C# 12 + + + + + +--- + +# v4 ReadMe(will DELETE). + +--- + + ConsoleAppFramework is an infrastructure of creating CLI(Command-line interface) tools, daemon, and multi batch application. You can create full feature of command line tool on only one-line. ![image](https://user-images.githubusercontent.com/46207/147662718-f7756523-67a9-4295-b090-3cfc94203017.png) diff --git a/sandbox/CliFrameworkBenchmark/CliFrameworkBenchmark.csproj b/sandbox/CliFrameworkBenchmark/CliFrameworkBenchmark.csproj index 359b7ae..e8e750e 100644 --- a/sandbox/CliFrameworkBenchmark/CliFrameworkBenchmark.csproj +++ b/sandbox/CliFrameworkBenchmark/CliFrameworkBenchmark.csproj @@ -6,6 +6,7 @@ enable annotations true + false diff --git a/sandbox/GeneratorSandbox/GeneratorSandbox.csproj b/sandbox/GeneratorSandbox/GeneratorSandbox.csproj index cb5b690..d4d4a9f 100644 --- a/sandbox/GeneratorSandbox/GeneratorSandbox.csproj +++ b/sandbox/GeneratorSandbox/GeneratorSandbox.csproj @@ -7,6 +7,7 @@ enable true 1701;1702;CS8321 + false diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index c31d8aa..f866434 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -1,285 +1,16 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Data; +using System.ComponentModel.DataAnnotations; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Net.Http; -using System.Net.Http.Headers; using System.Numerics; -using System.Reflection; -using System.Reflection.Metadata; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Text; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; using ConsoleAppFramework; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using static ConsoleAppFramework.ConsoleApp; -var builder = ConsoleApp.Create(); -builder.Add(); -builder.Run(args); +//args = ["--foo", "10", "--bar", "20"]; -public class MyClass() -{ - [Command("nomunomu")] - public void Do() - { - Console.Write("yeah"); - } -} - - - - - - - - - - - - - - - - - - - - - - - - -//builder.Add("foo/tako", (int x, int y) => { return "foo"; }); -//builder.Add("foo/tako/ekkusu", (int x, int y, int z) => { return "foo"; }); - - -// builder.Run(args); - -// var s = "foo"; -// s.AsSpan().Split(',',). - - -// BigInteger.TryParse( -// Version.TryParse( - -// Uri.TryCreate(UriCreationOptions - - -// IParsable.TryParse( - -// --x - - - - - - - - -[ConsoleAppFilter] -public class MyClass -{ - public void Do(CancellationToken cancellationToken) - { - Console.Write("yeah"); - } - - [ConsoleAppFilter] - public void Sum(int x, int y) - { - Console.WriteLine(x + y); - } - - public void Echo(string msg) - { - Console.Write(msg); - } - - void Echo() - { - } - - public static void Sum() - { - } - - -} - -public class MyCommands : IDisposable -{ - public int MyProperty { get; set; } - - public void Foo(int x) - { - Console.WriteLine("MyCommands.Foo:" + x); - } - - public int Boo() => 1; - - public static void Tako() - { - } - - private void Bar() - { - } - public ValueTask DisposeAsync() - { - throw new NotImplementedException(); - } - - public void Dispose() - { - Console.WriteLine("Disposed"); - } -} - -// constructor injection! +ConsoleApp.Run(args, (int foo, int bar) => Console.WriteLine($"Sum: {foo + bar}")); -public delegate void FooBar(int x, int y = 10); - -public partial struct ConsoleAppBuilderTest -{ - public void Add(string commandName, Delegate command) - { - AddCore(commandName, command); - } - - [Conditional("DEBUG")] - public void Add() { } - - [Conditional("DEBUG")] - public void Add(string commandName) { } - - 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); -} - -//partial struct ConsoleAppBuilderTest -//{ -// Action command1; - -// // root command => / -// // sub command => foo/bar/baz -// public void Add(string commandName, Action command) // generate Add methods -// { -// // multi -// switch (commandName) -// { -// case "foo": -// this.command1 = command; -// break; -// } -// } - -// // or RunAsync -// public partial void Run(string[] args) // generate body -// { -// // --help? - -// switch (args[0]) -// { -// case "foo": -// RunCommand1(args.AsSpan(1), command1); -// break; -// case "bar": - -// break; -// } -// } - -// public partial Task RunAsync(string[] args) => throw new NotImplementedException(); - -// // generate both invoke and invokeasync? detect which calls? -// // void Invoke -// static void RunCommand1(Span args, Action command) -// { -// // call generated... -// } -//} - - - -public static class Command -{ - /// - /// - /// - /// -f|--cho|/tako|*nano|-ZOMBI - /// - public static void Execute(int foo, CancellationToken cancellationToken) - { - } -} - - - -namespace Takoyaki -{ - public enum MyEnum - { - - } - - public static class Hoge - { - public static void Nano(int x) - { - - } - } -} - - - -//public class MyClass -//{ -// /// --tako, -t, foo bar baz. -// public void Foo(int takoyaki, int y) -// { -// } -//} - - -//public interface IArgumentParser -//{ -// static abstract bool TryParse(ReadOnlySpan s, out T result); -//} - - -[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] -internal sealed class ArgumentAttribute : Attribute -{ -} @@ -312,251 +43,66 @@ public static bool TryParse(ReadOnlySpan s, out Vector3 result) -[AttributeUsage(AttributeTargets.Parameter)] -internal sealed class ArrayParserAttribute : Attribute, IArgumentParser - where T : ISpanParsable +public class FilterContext : IServiceProvider { - public static bool TryParse(ReadOnlySpan s, out T[] result) - { - var count = s.Count(',') + 1; - result = new T[count]; - - var source = s; - var destination = result.AsSpan(); - Span ranges = stackalloc Range[Math.Min(count, 128)]; + public long Timestamp { get; set; } + public Guid UserId { get; set; } - while (true) - { - var splitCount = source.Split(ranges, ','); - var parseTo = splitCount; - if (splitCount == 128 && source[ranges[^1]].Contains(',')) // check have more region - { - 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; + object IServiceProvider.GetService(Type serviceType) + { + if (serviceType == typeof(FilterContext)) return this; + throw new InvalidOperationException("Type is invalid:" + serviceType); } } - -namespace ConsoleAppFramework +internal class TimestampFilter(ConsoleAppFilter next) + : ConsoleAppFilter(next) { - partial class ConsoleApp + public override Task InvokeAsync(CancellationToken cancellationToken) { - //static bool TryParseParamsArray(ReadOnlySpan args, ref string[] result, ref int i) - //{ - // result = new string[args.Length - i]; - // var resultIndex = 0; - // for (; i < args.Length; i++) - // { - // result[resultIndex++] = args[++i]; - // } - // return true; - //} - - //static bool TryParseParamsArray(ReadOnlySpan args, ref T[] result, ref int i) - // where T : ISpanParsable - //{ - // 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; - //} - - - - partial struct ConsoleAppBuilder - { - - - // public void UseFilter() where T : ConsoleAppFilter { } - - - //public class ConsoleAppBuilder - //{ - // public void Add() - // { - // } - - - - // public void Run(string[] args) - // { - // if (args.Length == 0 || args[0].StartsWith('-')) - // { - // // invoke root command - // } - // } - - // public void RunAsync(string[] args) - // { - // } - //} - - partial class ConsoleApp - { - private static async Task RunAsyncCommand1(string[] args) - { - // if (TryShowHelpOrVersion(args, 0)) return; - - using var posixSignalHandler = PosixSignalHandler.Register(Timeout); - var arg0 = posixSignalHandler.Token; - - try - { - for (int i = 0; i < args.Length; i++) - { - var name = args[i]; - - switch (name) - { - default: - ThrowArgumentNameNotFound(name); - break; - } - } - var instance = new global::MyClass(); - await Task.Run(() => instance.Do(arg0!)).WaitAsync(posixSignalHandler.TimeoutToken); - } - catch (Exception ex) - { - if ((ex is OperationCanceledException oce) && (oce.CancellationToken == posixSignalHandler.Token || oce.CancellationToken == posixSignalHandler.TimeoutToken)) - { - Environment.ExitCode = 130; - return; - } - - Environment.ExitCode = 1; - if (ex is System.ComponentModel.DataAnnotations.ValidationException) - { - LogError(ex.Message); - } - else - { - LogError(ex.ToString()); - } - } - } - - - - public struct Builder() - { - - } - } - } - - - - public class FilterContext : IServiceProvider - { - public long Timestamp { get; set; } - public Guid UserId { get; set; } - - object IServiceProvider.GetService(Type serviceType) - { - if (serviceType == typeof(FilterContext)) return this; - throw new InvalidOperationException("Type is invalid:" + serviceType); - } - } - - //public abstract class ConsoleAppFilter(ConsoleAppFilter next) - //{ - // protected ConsoleAppFilter Next = next; - - // public abstract ValueTask InvokeAsync(CancellationToken cancellationToken); - //} - - //[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = false)] - //public sealed class ConsoleAppFilterAttribute : Attribute - // where T : ConsoleAppFilter - //{ - //} - - public class TimestampFilter(ConsoleAppFilter next) - : ConsoleAppFilter(next) - { - public override Task InvokeAsync(CancellationToken cancellationToken) - { - Console.WriteLine("filter1"); - return Next.InvokeAsync(cancellationToken); - } - } + Console.WriteLine("filter1"); + return Next.InvokeAsync(cancellationToken); + } +} - public class LogExecutionTimeFilter(ConsoleAppFilter next) - : ConsoleAppFilter(next) +internal class LogExecutionTimeFilter(ConsoleAppFilter next) + : ConsoleAppFilter(next) +{ + public override async Task InvokeAsync(CancellationToken cancellationToken) + { + var startingTime = Stopwatch.GetTimestamp(); + try { - public override async Task InvokeAsync(CancellationToken cancellationToken) - { - var startingTime = Stopwatch.GetTimestamp(); - try - { - await Next.InvokeAsync(cancellationToken); - } - finally - { - var elapsed = Stopwatch.GetElapsedTime(startingTime); - ConsoleApp.Log($"Execution Time: {elapsed}"); - } - } + await Next.InvokeAsync(cancellationToken); } - - public class NanimosinaiFilter(ConsoleAppFilter next) - : ConsoleAppFilter(next) + finally { - public override Task InvokeAsync(CancellationToken cancellationToken) - { - Console.WriteLine("filter0"); - return Next.InvokeAsync(cancellationToken); - } + var elapsed = Stopwatch.GetElapsedTime(startingTime); + ConsoleApp.Log($"Execution Time: {elapsed}"); } + } +} +internal class NanimosinaiFilter(ConsoleAppFilter next) + : ConsoleAppFilter(next) +{ + public override Task InvokeAsync(CancellationToken cancellationToken) + { + Console.WriteLine("filter0"); + return Next.InvokeAsync(cancellationToken); + } +} - public class MyContext : IServiceProvider - { - public long Timestamp { get; set; } - public Guid UserId { get; set; } - - object IServiceProvider.GetService(Type serviceType) - { - if (serviceType == typeof(MyContext)) return this; - throw new InvalidOperationException("Type is invalid:" + serviceType); - } - } - public class MyClass23 - { - public void Do() - { - Console.Write("yeah:"); - } - } +public class MyContext : IServiceProvider +{ + public long Timestamp { get; set; } + public Guid UserId { get; set; } - + object IServiceProvider.GetService(Type serviceType) + { + if (serviceType == typeof(MyContext)) return this; + throw new InvalidOperationException("Type is invalid:" + serviceType); } -} \ No newline at end of file +} diff --git a/src/ConsoleAppFramework/ConsoleAppGenerator.cs b/src/ConsoleAppFramework/ConsoleAppGenerator.cs index d35609d..d1d45e3 100644 --- a/src/ConsoleAppFramework/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework/ConsoleAppGenerator.cs @@ -320,13 +320,9 @@ static async Task RunWithFilterAsync(ConsoleAppFilter invoker) { await Task.Run(() => invoker.InvokeAsync(posixSignalHandler.Token)).WaitAsync(posixSignalHandler.TimeoutToken); } - catch (OperationCanceledException ex) when (ex.CancellationToken == posixSignalHandler.Token || ex.CancellationToken == posixSignalHandler.TimeoutToken) - { - Environment.ExitCode = 130; - } catch (Exception ex) { - if ((ex is OperationCanceledException oce) && (oce.CancellationToken == posixSignalHandler.Token || oce.CancellationToken == posixSignalHandler.TimeoutToken)) + if (ex is OperationCanceledException) { Environment.ExitCode = 130; return; diff --git a/src/ConsoleAppFramework/Emitter.cs b/src/ConsoleAppFramework/Emitter.cs index 617fe95..095db64 100644 --- a/src/ConsoleAppFramework/Emitter.cs +++ b/src/ConsoleAppFramework/Emitter.cs @@ -264,7 +264,7 @@ public void EmitRun(SourceBuilder sb, CommandWithId commandWithId, bool isRunAsy { if (hasCancellationToken) { - using (sb.BeginBlock("if ((ex is OperationCanceledException oce) && (oce.CancellationToken == posixSignalHandler.Token || oce.CancellationToken == posixSignalHandler.TimeoutToken))")) + using (sb.BeginBlock("if (ex is OperationCanceledException)")) { sb.AppendLine("Environment.ExitCode = 130;"); sb.AppendLine("return;"); From 3c68efccf2b0534f73236d5ba0201cde6d913fc6 Mon Sep 17 00:00:00 2001 From: neuecc Date: Fri, 31 May 2024 12:10:38 +0900 Subject: [PATCH 42/54] more --- ReadMe.md | 269 ++++++------------------------------------------------ 1 file changed, 28 insertions(+), 241 deletions(-) diff --git a/ReadMe.md b/ReadMe.md index 8c09a21..bbce9f9 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -103,265 +103,52 @@ Options: As you can see, the code is straightforward and simple, making it easy to imagine the execution cost of the framework portion. That's right, it's zero. This technique was influenced by Rust's macros. Rust has [Attribute-like macros and Function-like macros](https://doc.rust-lang.org/book/ch19-06-macros.html), and ConsoleAppFramework's generation can be considered as Function-like macros. +The `ConsoleApp` class, along with everything else, is generated entirely by the Source Generator, resulting in no dependencies, including ConsoleAppFramework itself. This characteristic should contribute to the small assembly size and ease of handling, including support for Native AOT. + +Moreover, CLI applications typically involve single-shot execution from a cold start. As a result, common optimization techniques such as dynamic code generation (IL Emit, ExpressionTree.Compile) and caching (ArrayPool) do not work effectively. ConsoleAppFramework generates everything statically in advance, achieving performance equivalent to optimized hand-written code without reflection or boxing. + +ConsoleAppFramework offers a rich set of features as a framework. The Source Generator analyzes which modules are being used and generates the minimal code necessary to implement the desired functionality. + +* SIGINT/SIGTERM(Ctrl+C) handling with gracefully shutdown via `CancellationToken` +* Filter(middleware) pipeline to intercept before/after execution +* Exit code management +* Support for async commands +* Registration of multiple commands +* Registration of nested commands +* Setting option aliases and descriptions from code document comment +* `System.ComponentModel.DataAnnotations` attribute-based Validation +* Dependency Injection for command registration by type and public methods +* High performance value parsing via `ISpanParsable` +* Parsing of params arrays +* Parsing of JSON arguments +* Help(`-h|--help`) option builder +* Default show version(`--version`) option +Getting Started +-- +This library is distributed via NuGet, minimal requirement is .NET 8 and C# 12. +> PM> Install-Package [ConsoleAppFramework](https://www.nuget.org/packages/ConsoleAppFramework) +ConsoleAppFramework is analyzer(Source Generator) and all generated types are internal. - - - -Dependency Injection, -async/await -Exit code, - -SIGINT/SIGTERM(Ctrl+C) handling with gracefully shutdown via CancellationToken, - -filter(middleware) pipeline, - -multi commands, -nested command, -options aliases, -params array, - -JSON argument, -help builder - - - - -Requirements - -.NET 8, C# 12 - - - - - ---- - -# v4 ReadMe(will DELETE). - ---- - - -ConsoleAppFramework is an infrastructure of creating CLI(Command-line interface) tools, daemon, and multi batch application. You can create full feature of command line tool on only one-line. - -![image](https://user-images.githubusercontent.com/46207/147662718-f7756523-67a9-4295-b090-3cfc94203017.png) - -This simplicity is by C# 10.0 and .NET 6 new features, similar as [ASP.NET Core 6.0 Minimal APIs](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis). - -Most minimal API is one-line(with top-level-statements, global-usings). - -```csharp -ConsoleApp.Run(args, (string name) => Console.WriteLine($"Hello {name}")); -``` - -Of course, ConsoleAppFramework has extensibility. - -```csharp -// Register two commands(use short-name, argument) -// hello -m -// sum [x] [y] -var app = ConsoleApp.Create(args); -app.AddCommand("hello", ([Option("m", "Message to display.")] string message) => Console.WriteLine($"Hello {message}")); -app.AddCommand("sum", ([Option(0)] int x, [Option(1)] int y) => Console.WriteLine(x + y)); -app.Run(); -``` - -You can register public method as command. This provides a simple way to registering multiple commands. - -```csharp -// AddCommands register as command. -// echo --msg --repeat(default = 3) -// sum [x] [y] -var app = ConsoleApp.Create(args); -app.AddCommands(); -app.Run(); - -public class Foo : ConsoleAppBase -{ - public void Echo(string msg, int repeat = 3) - { - for (var i = 0; i < repeat; i++) - { - Console.WriteLine(msg); - } - } - - public void Sum([Option(0)]int x, [Option(1)]int y) - { - Console.WriteLine((x + y).ToString()); - } -} -``` - -If you have many commands, you can define class separetely and use `AddAllCommandType` to register all commands one-line. - ```csharp -// Register `Foo` and `Bar` as SubCommands(You can also use AddSubCommands to register manually). -// foo echo --msg -// foo sum [x] [y] -// bar hello2 -var app = ConsoleApp.Create(args); -app.AddAllCommandType(); -app.Run(); - -public class Foo : ConsoleAppBase -{ - public void Echo(string msg) - { - Console.WriteLine(msg); - } - - public void Sum([Option(0)]int x, [Option(1)]int y) - { - Console.WriteLine((x + y).ToString()); - } -} +using ConsoleAppFramework; -public class Bar : ConsoleAppBase -{ - public void Hello2() - { - Console.WriteLine("H E L L O"); - } -} +ConsoleApp.Run(args, (string name) => Console.WriteLine($"Hello {name}")); ``` - ConsoleAppFramework is built on [.NET Generic Host](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host), you can use configuration, logging, DI, lifetime management by Microsoft.Extensions packages. ConsoleAppFramework do parameter binding from string args, routing many commands, dotnet style help builder, etc. - -![image](https://user-images.githubusercontent.com/46207/72047323-a08e0c80-32fd-11ea-850a-7f926adf3d22.png) - -Here is the full-sample of power of ConsoleAppFramework. - -```csharp -// You can use full feature of Generic Host(same as ASP.NET Core). - -var builder = ConsoleApp.CreateBuilder(args); -builder.ConfigureServices((ctx,services) => -{ - // Register EntityFramework database context - services.AddDbContext(); - - // Register appconfig.json to IOption - services.Configure(ctx.Configuration); - - // Using Cysharp/ZLogger for logging to file - services.AddLogging(logging => - { - logging.AddZLoggerFile("log.txt"); - }); -}); - -var app = builder.Build(); - -// setup many command, async, short-name/description option, subcommand, DI -app.AddCommand("calc-sum", (int x, int y) => Console.WriteLine(x + y)); -app.AddCommand("sleep", async ([Option("t", "seconds of sleep time.")] int time) => -{ - await Task.Delay(TimeSpan.FromSeconds(time)); -}); -app.AddSubCommand("verb", "childverb", () => Console.WriteLine("called via 'verb childverb'")); - -// You can insert all public methods as sub command => db select / db insert -// or AddCommand() all public methods as command => select / insert -app.AddSubCommands(); - -// some argument from DI. -app.AddRootCommand((ConsoleAppContext ctx, IOptions config, string name) => { }); - -app.Run(); - -// ---- - -[Command("db")] -public class DatabaseApp : ConsoleAppBase, IAsyncDisposable -{ - readonly ILogger logger; - readonly MyDbContext dbContext; - readonly IOptions config; +You can execute command like `sampletool --name "foo"`. - // you can get DI parameters. - public DatabaseApp(ILogger logger,IOptions config, MyDbContext dbContext) - { - this.logger = logger; - this.dbContext = dbContext; - this.config = config; - } - [Command("select")] - public async Task QueryAsync(int id) - { - // select * from... - } - // also allow defaultValue. - [Command("insert")] - public async Task InsertAsync(string value, int id = 0) - { - // insert into... - } - // support cleanup(IDisposable/IAsyncDisposable) - public async ValueTask DisposeAsync() - { - await dbContext.DisposeAsync(); - } -} -public class MyConfig -{ - public string FooValue { get; set; } = default!; - public string BarValue { get; set; } = default!; -} -``` -ConsoleAppFramework can create easily to many command application. Also enable to use GenericHost configuration is best way to share configuration/workflow when creating batch application for other .NET web app. If tool is for CI, git pull and run by `dotnet run -- [Command] [Option]` is very helpful. - -dotnet's standard CommandLine api - [System.CommandLine](https://github.com/dotnet/command-line-api) is low level, require many boilerplate codes. ConsoleAppFramework is like ASP.NET Core in CLI Applications, no needs boilerplate. However, with the power of Generic Host, it is simple and easy, but much more powerful. - - - -## Table of Contents - -- [Getting Started](#getting-started) -- [ConsoleApp / ConsoleAppBuilder](#consoleapp--consoleappbuilder) -- [Delegate convention](#delegate-convention) -- [AddCommand](#addcommand) - - [`AddRootCommand`](#addrootcommand) - - [`AddCommand` / `AddCommands`](#addcommand--addcommandst) - - [`AddSubCommand` / `AddSubCommands`](#addsubcommand--addsubcommandst) - - [`AddAllCommandType`](#addallcommandtype) -- [Complex Argument](#complex-argument) -- [Exit Code](#exit-code) -- [Implicit Using](#implicit-using) -- [CommandAttribute](#commandattribute) -- [OptionAttribute](#optionattribute) -- [Command parameters validation](#command-parameters-validation) -- [Daemon](#daemon) -- [Abort Timeout](#abort-timeout) -- [Filter](#filter) -- [Logging](#logging) -- [Configuration](#configuration) -- [DI](#di) -- [Cleanup](#cleanup) -- [ConsoleAppContext](#consoleappcontext) -- [ConsoleAppOptions](#consoleappoptions) -- [Terminate handling in Console.Read](#terminate-handling-in-consoleread) -- [Publish to executable file](#publish-to-executable-file) -- [v3 Legacy Compatibility](#v3-legacy-compatibility) -- [License](#license) - - -Getting Started --- -NuGet: [ConsoleAppFramework](https://www.nuget.org/packages/ConsoleAppFramework) -``` -Install-Package ConsoleAppFramework ``` If you are using .NET 6, automatically enabled implicit global `using ConsoleAppFramework;`. So you can write one line code. From f9fb81b3dd8fadc27a1ec1117ae8ed0e9cce1a49 Mon Sep 17 00:00:00 2001 From: neuecc Date: Fri, 31 May 2024 16:26:46 +0900 Subject: [PATCH 43/54] d --- .github/workflows/toc.yml | 15 - ReadMe.md | 993 ++++----------------- sandbox/CliFrameworkBenchmark/Benchmark.cs | 1 - sandbox/GeneratorSandbox/Program.cs | 33 +- 4 files changed, 208 insertions(+), 834 deletions(-) delete mode 100644 .github/workflows/toc.yml diff --git a/.github/workflows/toc.yml b/.github/workflows/toc.yml deleted file mode 100644 index fb0942c..0000000 --- a/.github/workflows/toc.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: TOC Generator - -on: - push: - paths: - - 'ReadMe.md' - -jobs: - generateTOC: - name: TOC Generator - runs-on: ubuntu-latest - steps: - - uses: technote-space/toc-generator@v2.4.0 - with: - TOC_TITLE: "## Table of Contents" diff --git a/ReadMe.md b/ReadMe.md index bbce9f9..6a23fa9 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -130,8 +130,9 @@ This library is distributed via NuGet, minimal requirement is .NET 8 and C# 12. > PM> Install-Package [ConsoleAppFramework](https://www.nuget.org/packages/ConsoleAppFramework) -ConsoleAppFramework is analyzer(Source Generator) and all generated types are internal. +ConsoleAppFramework is an analyzer (Source Generator) and does not have any dll references. When referenced, the entry point class `ConsoleAppFramework.ConsoleApp` is generated internally. +The first argument of `Run` or `RunAsync` can be `string[] args`, and the second argument can be any lambda expression, method, or function reference. Based on the content of the second argument, the corresponding function is automatically generated. ```csharp using ConsoleAppFramework; @@ -141,987 +142,347 @@ ConsoleApp.Run(args, (string name) => Console.WriteLine($"Hello {name}")); You can execute command like `sampletool --name "foo"`. +* The return value can be `void`, `int`, `Task`, or `Task` + * If an `int` is returned, that value will be set to `Environment.ExitCode` +* By default, option argument names are converted to `--lower-kebab-case` + * For example, `XmlReader` becomes `xml-reader` + * Option argument names are case-insensitive, but lower-case matches faster - - - - - - - -``` - -If you are using .NET 6, automatically enabled implicit global `using ConsoleAppFramework;`. So you can write one line code. - -```csharp -ConsoleApp.Run(args, (string name) => Console.WriteLine($"Hello {name}")); -``` - -You can execute command like `sampletool --name "foo"`. - -The Option parser is no longer needed. You can also use the `OptionAttribute` to describe the parameter and set short-name. +When passing a method, you can write it as follows: ```csharp -ConsoleApp.Run(args, ([Option("n", "name of send user.")] string name) => Console.WriteLine($"Hello {name}")); -``` - -``` -Usage: sampletool [options...] - -Options: - -n, --name name of user. (Required) +ConsoleApp.Run(args, Sum); -Commands: - help Display help. - version Display version. +void Sum(int x, int y) => Console.Write(x + y); ``` -Method parameter will be required parameter, optional parameter will be oprional parameter with default value. Also support boolean flag, if parameter is bool, in default it will be optional parameter and with `--foo` set true to parameter. +Additionally, for static functions, you can pass them as function pointers. In that case, the managed function pointer arguments will be generated, resulting in maximum performance. ```csharp -// lambda expression does not support default value so require to use local function -static void Hello([Option("m")]string message, [Option("e")] bool end, [Option("r")] int repeat = 3) +unsafe { - for (int i = 0; i < repeat; i++) - { - Console.WriteLine(message); - } - if (end) - { - Console.WriteLine("END"); - } + ConsoleApp.Run(args, &Sum); } -ConsoleApp.Run(args, Hello); +static void Sum(int x, int y) => Console.Write(x + y); ``` ```csharp -Options: - -m, --message (Required) - -e, --end (Optional) - -r, --repeat (Default: 3) -``` - -`help` command (or no argument to pass) and `version` command is enabled in default(You can disable this in options or can override by add same name of command). Also enables `command --help` option. This help format is similar as `dotnet` command, `version` command shows `AssemblyInformationalVersion` or `AssemblylVersion`. - +public static unsafe void Run(string[] args, delegate* managed command) ``` -> sampletool help -Usage: sampletool [options...] -Options: - -n, --name name of user. (Required) - -r, --repeat repeat count. (Default: 3) - -Commands: - help Display help. - version Display version. -``` - -``` -> sampletool version -1.0.0 -``` +Unfortunately, currently [static lambdas cannot be assigned to function pointers](https://github.com/dotnet/csharplang/discussions/6746), so defining a named function is necessary. -You can use `Run` or `AddCommands` to add multi commands easily. +When defining an asynchronous method using a lambda expression, the `async` keyword is required. ```csharp -ConsoleApp.Run(args); - -// require to inherit ConsoleAppBase -public class MyCommands : ConsoleAppBase +// --foo, --bar +await ConsoleApp.RunAsync(args, async (int foo, int bar, CancellationToken cancellationToken) => { - // You can receive DI services in constructor. - - // All public methods is registred. - - // Using [RootCommand] attribute will be root-command - [RootCommand] - public void Hello( - [Option("n", "name of send user.")] string name, - [Option("r", "repeat count.")] int repeat = 3) - { - for (int i = 0; i < repeat; i++) - { - Console.WriteLine($"Hello My ConsoleApp from {name}"); - } - } - - // [Option(int)] describes that parameter is passed by index - [Command("escape")] - public void UrlEscape([Option(0)] string input) - { - Console.WriteLine(Uri.EscapeDataString(input)); - } - - // define async method returns Task - [Command("timer")] - public async Task Timer([Option(0)] uint waitSeconds) - { - Console.WriteLine(waitSeconds + " seconds"); - while (waitSeconds != 0) - { - // ConsoleAppFramework does not stop immediately on terminate command(Ctrl+C) - // for allows gracefully shutdown(keeping safe cleanup) - // so you should pass Context.CancellationToken to async method. - // If not, abort timeout by HostOptions.ShutdownTimeout(default is 00:00:05). - await Task.Delay(TimeSpan.FromSeconds(1), Context.CancellationToken); - waitSeconds--; - Console.WriteLine(waitSeconds + " seconds"); - } - } -} -``` - -You can call like - -``` -sampletool -n "foo" -r 3 -sampletool escape http://foo.bar/ -sampletool timer 10 -``` - -This is recommended way to register multi commands. - -If you omit `[Command]` attribute, command and option name is used by there name and convert to `kebab-case` in default. - -```csharp - -// Command is url-escape -// Option is --input-file -public void UrlEscape(string inputFile) -{ -} + await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken); + Console.WriteLine($"Sum: {foo + bar}"); +}); ``` -This converting behaviour can configure by `ConsoleAppOptions.NameConverter`. +You can use either the `Run` or `RunAsync` method for invocation. It is optional to use `CancellationToken` as an argument. This becomes a special parameter and is excluded from the command options. Internally, it uses [`PosixSignalRegistration`](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.posixsignalregistration) to handle `SIGINT`, `SIGTERM`, and `SIGKILL`. When these signals are invoked (e.g., Ctrl+C), the CancellationToken is set to CancellationRequested. If `CancellationToken` is not used as an argument, these signals will not be handled, and the program will terminate immediately. For more details, refer to the [CancellationToken and Gracefully Shutdown](#cancellationtokengracefully-shutdown-and-timeout) section. -ConsoleApp / ConsoleAppBuilder +Option aliases and Help, Version --- -`ConsoleApp` is an entrypoint of creating ConsoleAppFramework app. It has three APIs, `Create`, `CreateBuilder`, `CreateFromHostBuilder` and `Run`. +By default, if `-h` or `--help` is provided, or if no arguments are passed, the help display will be invoked. ```csharp -// Create is shorthand of CraeteBuilder(args).Build(); -var app = ConsoleApp.Create(args); - -// Builder returns IHost so you can configure application hosting option. -var app = ConsoleApp.CreateBuilder(args) - .ConfigureServices(services => - { - }) - .Build(); - -// Run is shorthand of Create(args).AddRootCommand(rootCommand).Run(); -// If you want to create simple app, this API is faster. -ConsoleApp.Run(args, /* lambda expression */); - -// Run is shorthand of Create(args).AddCommands().Run(); -// AddCommands is recommend option to register many commands. -ConsoleApp.Run(args); +ConsoleApp.Run(args, (string message) => Console.Write($"Hello, {message}")); ``` -When calling `Create/CreateBuilder/CreateFromHostBuilder`, also configure `ConsoleAppOptions`. Full option details, see [ConsoleAppOptions](#consoleappoptions) section. - -```csharp -var app = ConsoleApp.Create(args, options => -{ - options.ShowDefaultCommand = false; - options.NameConverter = x => x.ToLower(); -}); -``` - -Advanced API of `ConsoleApp`, `CreateFromHostBuilder` creates ConsoleApp from IHostBuilder. +```txt +Usage: [options...] [-h|--help] [--version] -```csharp -// Setup services outside of ConsoleAppFramework. -var hostBuilder = Host.CreateDefaultBuilder() - .ConfigureServices(); - -var app = ConsoleApp.CreateFromHostBuilder(hostBuilder); +Options: + --message (Required) ``` -`ConsoleAppBuilder` itself is `IHostBuilder` so you can use any configuration methods like `ConfigureServices`, `ConfigureLogging`, etc. If method chain is not returns `ConsoleAppBuilder`(for example, using external lib's extension methods), can not get `ConsoleApp` directly. In that case, use `BuildAsConsoleApp()` instead of `Build()`. - -`ConsoleApp` exposes some utility properties. - -* `IHost` Host -* `ILogger` Logger -* `IServiceProvider` Services -* `IConfiguration` Configuration -* `IHostEnvironment` Environment -* `IHostApplicationLifetime` Lifetime - -`Run()` and `RunAsync(CancellationToken)` to finally invoke application. Run is shorthand of `RunAsync().GetAwaiter().GetResult()` so receives same result of `await RunAsync()`. On Entrypoint, there is not much need to do `await RunAsync()`. Therefore, it is usually a good to choose `Run()`. - -Delegate convention ---- -`AddCommand` accepts `Delegate` in argument. In C# 10.0 allows naturaly syntax of lambda expressions. +In ConsoleAppFramework, instead of using attributes, you can provide descriptions and aliases for functions by writing Document Comments. This avoids the common issue in frameworks where arguments become cluttered with attributes, making the code difficult to read. With this approach, a natural writing style is achieved. ```csharp -app.AddCommand("no-argument", () => { }); -app.AddCommand("any-arguments", (int x, string y, TimeSpan z) => { }); -app.AddCommand("instance", new MyClass().Cmd); -app.AddCommand("async", async () => { }); -app.AddCommand("attribute", ([Option("msg")]string message) => { }); +ConsoleApp.Run(args, Commands.Hello); -static void Hello1() { } -app.AddCommand("local-static", Hello1); - -void Hello2() { } -app.AddCommand("local-method", Hello2); - -async Task Async() { } -app.AddCommand("async-method", Async); - -void OptionalParameter(int x = 10, int y = 20) { } -app.AddCommand("optional", OptionalParameter); - -public class MyClass +static class Commands { - public void Cmd() - { - Console.WriteLine("OK"); - } + /// + /// Display Hello. + /// + /// -m, Message to show. + public static void Hello(string message) => Console.Write($"Hello, {message}"); } ``` -lambda expressions can not use optional parameter so if you want to need it, using local/static functions. +```txt +Usage: [options...] [-h|--help] [--version] -Delegate(both lambda and method) allows to receive `ConsoleAppContext` or any your DI types. DI types is ignored as parameter. +Display Hello. -```csharp -// option is --param1, --param2 -app.AddCommand("di", (ConsoleAppContext ctx, ILogger logger, int param1, int param2) => { }); +Options: + -m|--message Message to show. (Required) ``` -AddCommand ---- -### `AddRootCommand` - -`RootCommand` means default(no command name) command of application. `ConsoleApp.Run(Delegate)` uses root command. +To add aliases to parameters, list the aliases separated by `|` before the comma in the comment. For example, if you write a comment like `-a|-b|--abcde, Description.`, then `-a`, `-b`, and `--abcde` will be treated as aliases, and `Description.` will be the description. -### `AddCommand` / `AddCommands` +Unfortunately, due to current C# specifications, lambda expressions and [local functions do not support document comments](https://github.com/dotnet/csharplang/issues/2110), so a class is required. -`AddCommand` requires first argument as command-name. `AddCommands` allows to register many command via `ConsoleAppBase` `ConsoleAppBase` has `Context`, it has executing information and `CancellationToken`. +In addition to `-h|--help`, there is another special built-in option: `--version`. This displays the `AssemblyInformationalVersion` or `AssemblyVersion`. -```csharp -// Commands: -// hello -// world -app.AddCommands(); -app.Run(); - -// Inherit ConsoleAPpBase -public class MyCommands : ConsoleAppBase, IDisposable -{ - readonly ILogger logger; - - // You can receive DI services in constructor. - public MyCommands(ILogger logger) - { - this.logger = logger; - } - - // All public methods is registered. - - // Using [RootCommand] attribute will be root-command - [RootCommand] - public void Hello() - { - // Context has any useful information. - Console.WriteLine(this.Context.Timestamp); - } - - public async Task World() - { - await Task.Delay(1000, this.Context.CancellationToken); - } - - // If implements IDisposable, called for cleanup - public void Dispose() - { - } -} -``` - -### `AddSubCommand` / `AddSubCommands` - -`AddSubCommand(string parentCommandName, string commandName, Delegate command)` registers nested command. +Command +--- +If you want to register multiple commands or perform complex operations (such as adding filters), instead of using `Run/RunAsync`, obtain the `ConsoleAppBuilder` using `ConsoleApp.Create()`. Call `Add`, `Add`, or `UseFilter` multiple times on the `ConsoleAppBuilder` to register commands and filters, and finally execute the application using `Run` or `RunAsync`. ```csharp -// Commands: -// foo bar1 -// foo bar2 -// foo bar3 -app.AddSubCommand("foo", "bar1", () => { }); -app.AddSubCommand("foo", "bar2", () => { }); -app.AddSubCommand("foo", "bar3", () => { }); -``` +var app = ConsoleApp.Create(); -`AddSubCommands` is similar as `AddCommands` but used type-name(or `[Command]` name) as parentCommandName. +app.Add("", (string msg) => Console.WriteLine(msg)); +app.Add("echo", (string msg) => Console.WriteLine(msg)); +app.Add("sum", (int x, int y) => Console.WriteLine(x + y)); -```csharp -// Commands: -// my-commands hello -// my-commands world -app.AddSubCommands(); +// --msg +// echo --msg +// sum --x --y +app.Run(args); ``` -### `AddAllCommandType` +The first argument of `Add` is the command name. If you specify an empty string `""`, it becomes the root command. Unlike parameters, command names are case-sensitive and cannot have multiple names. -`AddAllCommandType` searches all `ConsoleAppBase` type in assembly and register by `AddSubCommands`. +With `Add`, you can add multiple commands at once using a class-based approach, where public methods are treated as commands. If you want to write document comments for multiple commands, this approach allows for cleaner code, so it is recommended. Additionally, as mentioned later, you can also write clean code for Dependency Injection (DI) using constructor injection. ```csharp -// Commands: -// foo echo -// foo sum -// bar hello2 -app.AddAllCommandType(); +var app = ConsoleApp.Create(); +app.Add(); +app.Run(args); -// Batches. -public class Foo : ConsoleAppBase +public class MyCommands { - public void Echo(string msg) - { - Console.WriteLine(msg); - } - - public void Sum(int x, int y) - { - Console.WriteLine((x + y).ToString()); - } -} + /// Root command test. + /// -m, Message to show. + [Command("")] + public void Root(string msg) => Console.WriteLine(msg); -public class Bar : ConsoleAppBase -{ - public void Hello2() - { - Console.WriteLine("H E L L O"); - } -} -``` - -This is most easy to create many commands so useful for application batch that requires many many command. - -Commands are searched from loaded assemblies(in default `AppDomain.CurrentDomain.GetAssemblies()`), when does not touch other assemblies type, it will be trimmed and can not load it. In that case, use `AddAllCommandType(params Assembly[] searchAssemblies)` overload to pass target assembly, for example `AddAllCommandType(typeof(Foo).Assembly)` preserve types. - -Complex Argument ---- -If the argument is not primitive, you can pass JSON string. - -```csharp -public class ComplexArgTest : ConsoleAppBase -{ - public void Foo(int[] array, Person person) - { - Console.WriteLine(string.Join(", ", array)); - Console.WriteLine(person.Age + ":" + person.Name); - } -} + /// Display message. + /// Message to show. + public void Echo(string msg) => Console.WriteLine(msg); -public class Person -{ - public int Age { get; set; } - public string Name { get; set; } + /// Sum parameters. + /// left value. + /// right value. + public void Sum(int x, int y) => Console.WriteLine(x + y); } ``` -You can call like here. - -``` -> sampletool -array [10,20,30] -person {"Age":10,"Name":"foo"} +When you check the registered commands with `--help`, it will look like this. Note that you can register multiple `Add` and also add commands using `Add`. -# including space, use escaping -> SampleApp.exe -array [10,20,30] -person "{\"Age\":10,\"Name\":\"foo bar\"}" -``` +```txt +Usage: [command] [options...] [-h|--help] [--version] -> be careful with JSON string double quotation. +Root command test. -For the array handling, it can be a treat without correct JSON. -e.g. one-length argument can handle without `[]`. +Options: + -m|--msg Message to show. (Required) -```csharp -Foo(int[] array) -> SampleApp.exe -array 9999 +Commands: + echo Display message. + sum Sum parameters. ``` -multiple-argument can handle by split with ` ` or `,`. - -```csharp -Foo(int[] array) -> SampleApp.exe -array "11 22 33" -> SampleApp.exe -array "11,22,33" -> SampleApp.exe -array "[11,22,33]" -``` +By default, the command name is derived from the method name converted to `lower-kebab-case`. However, you can change the name to any desired value using the `[Command(string commandName)]` attribute. -string argument can handle without `"`. +If the class implements `IDisposable` or `IAsyncDisposable`, the Dispose or DisposeAsync method will be called after the command execution. -```csharp -Foo(string[] array) -> SampleApp.exe -array hello -> SampleApp.exe -array "foo bar baz" -> SampleApp.exe -array foo,bar,baz -> SampleApp.exe -array "["foo","bar","baz"]" -``` +### Nested command -Exit Code ---- -If the method returns `int` or `Task` or `ValueTask value, ConsoleAppFramework will set the return value to the exit code. +You can create a deep command hierarchy by adding commands with paths separated by `/` when registering them. This allows you to add commands at nested levels. ```csharp -public class ExampleApp : ConsoleAppBase -{ - [Command("exit")] - public int ExitCode() - { - return 12345; - } - - [Command("exit-with-task")] - public async Task ExitCodeWithTask() - { - return 54321; - } -} -``` +var app = ConsoleApp.Create(); -> **NOTE**: If the method throws an unhandled exception, ConsoleAppFramework always set `1` to the exit code. +app.Add("foo", () => { }); +app.Add("foo/bar", () => { }); +app.Add("foo/bar/barbaz", () => { }); +app.Add("foo/baz", () => { }); -Implicit Using ---- -In .NET 6, `global using ConsoleAppFramework` is enabled in default. If you remove global using, setup this element to target `.csproj`. - -```xml - - - +// Commands: +// foo +// foo bar +// foo bar barbaz +// foo baz +app.Run(args); ``` -CommandAttribute ---- -`CommandAttribute` enables subscommand on `RunConsoleAppFramework()`(for single type CLI app), changes command name on `RunConsoleAppFramework()`(for muilti type command routing), also describes the description. +`Add` can also add commands to a hierarchy by passing a `string commandPath` argument. ```csharp -RunConsoleAppFramework(); - -public class App : ConsoleAppBase -{ - // as Root Command(no command argument) - public void Run() - { - } +var app = ConsoleApp.Create(); +app.Add("foo"); - [Command("sec", "sub comman of this app")] - public void Second() - { - } -} -``` - -```csharp -RunConsoleAppFramework(); - -public class App2 : ConsoleAppBase -{ - // routing command: `app2 exec` - [Command("exec", "exec app.")] - public void Exec1() - { - } -} - -// command attribute also can use to class. -[Command("mycmd")] -public class App3 : ConsoleAppBase -{ - // routing command: `mycmd e2` - [Command("e2", "exec app 2.")] - public void ExecExec() - { - } -} +// Commands: +// foo Root command test. +// foo echo Display message. +// foo sum Sum parameters. +app.Run(args); ``` -OptionAttribute +Parse and Value Binding --- -OptionAttribute configure parameter, it can set shortName or order index, and help description. - -If you want to add only description, set "" or null to shortName parameter. - -```csharp -public void Hello( - [Option("n", "name of send user.")]string name, - [Option("r", "repeat count.")]int repeat = 3) -{ -} - -[Command("escape")] -public void UrlEscape([Option(0, "input of this command")]string input) -{ -} - -[Command("unescape")] -public void UrlUnescape([Option(null, "input of this command")]string input) -{ -} -``` -## Command parameters validation -Values of command parameters can be validated via validation attributes from `System.ComponentModel.DataAnnotations` -namespace and custom ones inheriting `ValidationAttribute` type. +`[Argument]` -```csharp -using System.ComponentModel.DataAnnotations; -// ... +`bool` -internal class TestConsoleApp : ConsoleAppBase -{ - [Command("some-command")] - public void SomeCommand( - [EmailAddress] string firstArg, - [Range(0, 2)] int secondArg) => Console.WriteLine($"hello from {nameof(TestConsoleApp)}"); -} -``` -Output (command invoked with params [**--first-arg "invalid-email-address" --second-arg" 10**]) -``` -Some parameters have invalid values: -first-arg (invalid-email-address): The String field is not a valid e-mail address. -second-arg (10): The field Int32 must be between 0 and 2. -``` -Daemon ---- -If use infinite-loop, it becomes daemon program. `ConsoleAppContext.CancellationToken` is lifecycle token of application. You can check `CancellationToken.IsCancellationRequested` and shutdown gracefully. -```csharp -public class Daemon : ConsoleAppBase -{ - [RootCommand] - public async Task Run() - { - // you can write infinite-loop while stop request(Ctrl+C or docker terminate). - try - { - while (!this.Context.CancellationToken.IsCancellationRequested) - { - try - { - Console.WriteLine("Wait One Minutes"); - } - catch (Exception ex) - { - // error occured but continue to run(or terminate). - Console.WriteLine(ex, "Found error"); - } - - // wait for next time - await Task.Delay(TimeSpan.FromMinutes(1), this.Context.CancellationToken); - } - } - catch (Exception ex) when (!(ex is OperationCanceledException)) - { - // you can write finally exception handling(without cancellation) - } - finally - { - // you can write cleanup code here. - } - } -} -``` - -Abort Timeout ---- -ConsoleAppFramework's execution lifetime is managed via generic host. If you do cancel(Ctrl+C), host starts cancellation process with timeout. If you don't pass `CancellationToken` in async method, does not cancel immediately. - -```csharp -public async Task Wait1() -{ - // Not good. - await Task.Delay(TimeSpan.FromMinutes(60)); -} -public async Task Wait2() -{ - // Good. - await Task.Delay(TimeSpan.FromMinutes(60), this.Context.CancellationToken); -} -``` -Default timeout time is `00:00:05`, you can change via `ConfigureHostOptions`. -```csharp -var app = ConsoleApp.CreateBuilder(args) - .ConfigureHostOptions(options => - { - // change timeout. - options.ShutdownTimeout = TimeSpan.FromMinutes(30); - }) - .Build(); -``` +`enum` +`nullable?` +`DateTime` -Filter ---- -Filter can hook before/after batch running event. You can implement `ConsoleAppFilter` for it and attach to global/class/method. +`ISpanParsable` +#### default +#### json +#### params T[] -```csharp -public class MyFilter : ConsoleAppFilter -{ - // Filter is instantiated by DI so you can get parameter by constructor injection. - public async override ValueTask Invoke(ConsoleAppContext context, Func next) - { - try - { - /* on before */ - await next(context); // next - } - catch - { - /* on after */ - throw; - } - finally - { - /* on finally */ - } - } -} -``` +#### Custom Value Converter -`ConsoleAppContext.Timestamp` has start time so if subtraction from now, get elapsed time. +// TODO: ```csharp -public class LogRunningTimeFilter : ConsoleAppFilter +[AttributeUsage(AttributeTargets.Parameter)] +public class Vector3ParserAttribute : Attribute, IArgumentParser { - public override async ValueTask Invoke(ConsoleAppContext context, Func next) + public static bool TryParse(ReadOnlySpan s, out Vector3 result) { - context.Logger.LogInformation("Call method at " + context.Timestamp.ToLocalTime()); // LocalTime for human readable time - try + Span ranges = stackalloc Range[3]; + var splitCount = s.Split(ranges, ','); + if (splitCount != 3) { - await next(context); - context.Logger.LogInformation("Call method Completed successfully, Elapsed:" + (DateTimeOffset.UtcNow - context.Timestamp)); + result = default; + return false; } - catch - { - context.Logger.LogInformation("Call method Completed Failed, Elapsed:" + (DateTimeOffset.UtcNow - context.Timestamp)); - throw; - } - } -} -``` - -In default, ConsoleAppFramework does not prevent double startup but if create filter, can do. -```csharp -public class MutexFilter : ConsoleAppFilter -{ - public override async ValueTask Invoke(ConsoleAppContext context, Func next) - { - var name = context.MethodInfo.DeclaringType.Name + "." + context.MethodInfo.Name; - using (var mutex = new Mutex(true, name, out var createdNew)) + float x; + float y; + float z; + if (float.TryParse(s[ranges[0]], out x) && float.TryParse(s[ranges[1]], out y) && float.TryParse(s[ranges[2]], out z)) { - if (!createdNew) - { - throw new Exception($"already running {name} in another process."); - } - - await next(context); + result = new Vector3(x, y, z); + return true; } - } -} -``` - -There filters can pass to `ConsoleAppOptions.GlobalFilters` on startup or attach by attribute on class, method. -```csharp -var app = ConsoleApp.Create(args, options => -{ - options.GlobalFilters = new ConsoleAppFilter[] - { - new MutextFilter() { Order = -9999 } , - new LogRunningTimeFilter() { Oder = -9998 }, - } -}); - -[ConsoleAppFilter(typeof(MyFilter3))] -public class MyBatch : ConsoleAppBase -{ - [ConsoleAppFilter(typeof(MyFilter4), Order = -9999)] - [ConsoleAppFilter(typeof(MyFilter5), Order = 9999)] - public void Do() - { + result = default; + return false; } } ``` -Execution order can control by `int Order` property. - -Logging +CancellationToken(Gracefully Shutdown) and Timeout --- -In default, `Context.Logger` has `ILogger` and `ILogger` can inject to constructor. Default `ConsoleLogger` format in `Host.CreateDefaultBuilder` is supernumerary and not suitable for console application. ConsoleAppFramework provides `SimpleConsoleLogger` to replace default ConsoleLogger in default. If you want to keep default `ConsoleLogger`, use `ConsoleAppOptions.ReplaceToUseSimpleConsoleLogger` to `false`. -If you want to use high performance logger/output to file, also use [Cysharp/ZLogger](https://github.com/Cysharp/ZLogger/) that easy to integrate ConsoleAppFramework. -```csharp -using ZLogger; -var app = ConsoleApp.CreateDefaultBuilder(args) - .ConfigureLogging(x => - { - x.ClearProviders(); // clear all providers - x.SetMinimumLevel(LogLevel.Trace); // change log level if you want - x.AddZLoggerConsole(); // add ZLogger Console - x.AddZLoggerFile("fileName.log"); // add ZLogger file output - }) - .Build(); -``` -Configuration +Exit Code --- -ConsoleAppFramework is just an infrastructure. You can add `appsettings.json` or other configs as .NET Core offers via `Microsoft.Extensions.Options`. -You can add `appsettings.json` and `appsettings.{environment}.json` and typesafe load via map config to Class w/IOption. +If the method returns `int` or `Task` or `ValueTask value, ConsoleAppFramework will set the return value to the exit code. -Here's single contained batch with Config loading sample. -```json -// appconfig.json(Content, Copy to Output Directory) -{ - "Foo": 42, - "Bar": true -} -``` -```csharp -using Microsoft.Extensions.DependencyInjection; +> **NOTE**: If the method throws an unhandled exception, ConsoleAppFramework always set `1` to the exit code. -var app = ConsoleApp.CreateBuilder(args) - .ConfigureServices((hostContext, services) => - { - // mapping config json to IOption - // requires "Microsoft.Extensions.Options.ConfigurationExtensions" package - // if you want to map subscetion in json, use Configure(hostContext.Configuration.GetSection("foo")) - services.Configure(hostContext.Configuration); - }) - .Build(); - -public class ConfigAppSample : ConsoleAppBase -{ - MyConfig config; - // get configuration from DI. - public ConfigAppSample(IOptions config) - { - this.config = config.Value; - } - public void ShowOption() - { - Console.WriteLine(config.Bar); - Console.WriteLine(config.Foo); - } -} -``` +Log +--- -for the details, please see [.NET Core Generic Host](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host) documentation. -DI +Attribute based parameters validation --- -You can use DI(constructor injection) by GenericHost. -```csharp -IOptions config; -ILogger logger; -public MyApp(IOptions config, ILogger logger) -{ - this.config = config; - this.logger = logger; -} -``` -DI also allows delegate registration. -```csharp -app.AddCommand("di", (ConsoleAppContext ctx, ILogger logger, int param1, int param2) => { }); -``` -DI also inject to filter. -Cleanup +Filter(Middleware) Pipline --- -You can implement `IDisposable.Dispose` or `IAsyncDisposable.DisposeAsync` explicitly, that is called after command finished. + +// TODO:samples? change exit code, log, etc... ```csharp -public class MyApp : ConsoleAppBase, IDisposable +internal class TimestampFilter(ConsoleAppFilter next) + : ConsoleAppFilter(next) { - public void Hello() - { - Console.WriteLine("Hello"); - } - - // Dispose/DisposeAsync method is not registered as Command. - public void Dispose() + public override Task InvokeAsync(CancellationToken cancellationToken) { - Console.WriteLine("DISPOSED"); + Console.WriteLine("filter1"); + return Next.InvokeAsync(cancellationToken); } } -``` -If implements both `IDisposable` and `IAsyncDisposable`, called only `IAsyncDisposable`. -```csharp -public class MyApp : ConsoleAppBase, IDisposable, IAsyncDisposable +internal class LogExecutionTimeFilter(ConsoleAppFilter next) + : ConsoleAppFilter(next) { - public void Hello() - { - Console.WriteLine("Hello"); - } - - public void Dispose() - { - Console.WriteLine("Not called."); - } - - public async ValueTask DisposeAsync() + public override async Task InvokeAsync(CancellationToken cancellationToken) { - Console.WriteLine("called."); + var startingTime = Stopwatch.GetTimestamp(); + try + { + await Next.InvokeAsync(cancellationToken); + } + finally + { + var elapsed = Stopwatch.GetElapsedTime(startingTime); + ConsoleApp.Log($"Execution Time: {elapsed}"); + } } } ``` -ConsoleAppContext ---- -ConsoleAppContext is injected to property on method executing. -```csharp -public class ConsoleAppContext -{ - public string?[] Arguments { get; } - public DateTime Timestamp { get; } - public CancellationToken CancellationToken { get; } - public ILogger Logger { get; } - public MethodInfo MethodInfo { get; } - public IServiceProvider ServiceProvider { get; } - public IDictionary Items { get; } - - public void Cancel(); - public void Terminate(); -} -``` -`Cancel()` set `CancellationToken` to canceled. Also `Terminate()` set token to cancled and terminate process(internal throws `OperationCanceledException` immediately). -ConsoleAppOptions +Dependency Injection(Logging, Configuration, etc...) --- -You can configure framework behaviour by ConsoleAppOptions. -```csharp -var app = ConsoleApp.Create(args, options => -{ - options.StrictOption = false, // default is true. - options.ShowDefaultCommand = false, // default is true -}); -``` - -```csharp -public class ConsoleAppOptions -{ - /// Argument parser uses strict(-short, --long) option. Default is true. - public bool StrictOption { get; set; } = true; - - /// Show default command(help/version) to help. Default is true. - public bool ShowDefaultCommand { get; set; } = true; - - public bool ReplaceToUseSimpleConsoleLogger { get; set; } = true; - - public JsonSerializerOptions? JsonSerializerOptions { get; set; } - - public ConsoleAppFilter[]? GlobalFilters { get; set; } - - public bool NoAttributeCommandAsImplicitlyDefault { get; set; } +`[FromServices]` - public Func NameConverter { get; set; } = KebabCaseConvert; - - public string? ApplicationName { get; set; } = null; -} -``` - -If StrictOption = false, does not distinguish between the number of `-`. For example, this method - -``` -public void Hello([Option("m", "Message to display.")]string message) -``` - -can pass argument by `-m`, `--message` and `-message`. This is styled like a go lang command. But if you want to strictly distinguish argument of `-`, set `StrcitOption = true`(default), that allows `-m` and `--message`. - -Also, by default, the `help` and `version` commands appear as help, which can be hidden by setting `ShowDefaultCommand = false`. - -NameConverter is used type-name, method-name, parameter-name converting as command. Default is convert to lower kebab-case. +// TODO: minimum single context ```csharp -// my-command query-data --organization-id --user-id -public class MyCommand +public class FilterContext : IServiceProvider { - public void QueryData(string organizationId, string userId); + public long Timestamp { get; set; } + public Guid UserId { get; set; } + + object IServiceProvider.GetService(Type serviceType) + { + if (serviceType == typeof(FilterContext)) return this; + throw new InvalidOperationException("Type is invalid:" + serviceType); + } } ``` -You can set func to change this behaviour like `NameConverter = x => x.ToLower();`. - -`ApplicationName` configure help usages `Usage: ***`, default(null) shows filename without extension. - -Terminate handling in Console.Read ---- -ConsoleAppFramework handle terminate signal(Ctrl+C) gracefully with `ConsoleAppContext.CancellationToken`. If your application waiting with Console.Read/ReadLine/ReadKey, requires additional handling. -```csharp -// case of Console.Read/ReadLine, pressed Ctrl+C, Read returns null. -ConsoleApp.Run(args, (ConsoleAppContext ctx) => -{ - var read = Console.ReadLine(); - if (read == null) ctx.Terminate(); -}); -``` -```csharp -// case of Console.ReadKey, can not cancel itself so use with Task.Run and WaitAsync. -ConsoleApp.Run(args, async (ConsoleAppContext ctx) => -{ - var key = await Task.Run(() => Console.ReadKey()).WaitAsync(ctx.CancellationToken); -}); -``` Publish to executable file --- -[dotnet run](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-run) is useful for local development or execute in CI tool. For example in CI, git pull and execute by `dotnet run -- --options` is easy to manage and execute utilities. - -[dotnet publish](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-publish) to create executable file. [.NET Core 3.0 offers Single Executable File](https://docs.microsoft.com/en-us/dotnet/core/whats-new/dotnet-core-3-0) via `PublishSingleFile`. - -CLI tool can use [.NET Core Local/Global Tools](https://docs.microsoft.com/en-us/dotnet/core/tools/global-tools). If you want to create it, check the [Tutorial: Create a .NET tool using the .NET CLI](https://docs.microsoft.com/en-us/dotnet/core/tools/global-tools-how-to-create) and [Use a global tool](https://docs.microsoft.com/en-us/dotnet/core/tools/global-tools-how-to-use) or [Use a local tool](https://docs.microsoft.com/en-us/dotnet/core/tools/local-tools-how-to-use). - -v3 Legacy Compatibility ---- -v1-v3 does not exist minimal api style(`ConsoleApp.Create/CreateBuilder`). - -```csharp -await Host.CreateDefaultBuilder() - .RunConsoleAppFrameworkAsync(args); -``` - -`RunConsoleAppFrameworkAsync` is still exists but does not recommend to use. Also, since v4, there is a change in the default behavior. When `RunConsoleAppFrameworkAsync` is used, the option settings of v3 and earlier will be used. - -```csharp -options.NoAttributeCommandAsImplicitlyDefault = true; -options.StrictOption = false; -options.NameConverter = x => x.ToLower(); -options.ReplaceToUseSimpleConsoleLogger = false; -``` -You can also get this option setting in `ConsoleAppOptions.CreateLegacyCompatible()`. +* Native AOT +* dotnet run +* dotnet publish License --- diff --git a/sandbox/CliFrameworkBenchmark/Benchmark.cs b/sandbox/CliFrameworkBenchmark/Benchmark.cs index 0a7fc0c..fdae49b 100644 --- a/sandbox/CliFrameworkBenchmark/Benchmark.cs +++ b/sandbox/CliFrameworkBenchmark/Benchmark.cs @@ -7,7 +7,6 @@ using CliFx; using Cocona.Benchmark.External.Commands; using CommandLine; -using ConsoleAppFramework; using PowerArgs; using Spectre.Console.Cli; using System.ComponentModel.DataAnnotations.Schema; diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index f866434..3e7125c 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -1,17 +1,46 @@ -using System.ComponentModel.DataAnnotations; +using System; +using System.ComponentModel.DataAnnotations; using System.Diagnostics; using System.Numerics; using System.Runtime.CompilerServices; using ConsoleAppFramework; //args = ["--foo", "10", "--bar", "20"]; +args = ["--help"]; +var app = ConsoleApp.Create(); -ConsoleApp.Run(args, (int foo, int bar) => Console.WriteLine($"Sum: {foo + bar}")); +app.Add("foo"); +// Commands: +// foo +// foo bar +// foo bar barbaz +// foo baz +app.Run(args); +public class MyCommands : IDisposable +{ + /// Root command test. + /// -m, Message to show. + [Command("")] + public void Root(string msg) => Console.WriteLine(msg); + + /// Display message. + /// Message to show. + public void Echo(string msg) => Console.WriteLine(msg); + + /// Sum parameters. + /// left value. + /// right value. + public void Sum(int x, int y) => Console.WriteLine(x + y); + public void Dispose() + { + Console.WriteLine("Disposed."); + } +} [AttributeUsage(AttributeTargets.Parameter)] From 2707d4fdaf4b668787df2de1a21fefbf88f80471 Mon Sep 17 00:00:00 2001 From: neuecc Date: Fri, 31 May 2024 16:35:42 +0900 Subject: [PATCH 44/54] rrr --- ReadMe.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ReadMe.md b/ReadMe.md index 6a23fa9..cf730b8 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -330,6 +330,7 @@ app.Run(args); Parse and Value Binding --- +// TODO:reason and policy of limitation of parsing `[Argument]` @@ -418,6 +419,7 @@ Filter(Middleware) Pipline --- // TODO:samples? change exit code, log, etc... +// TODO: how to share filter ```csharp internal class TimestampFilter(ConsoleAppFilter next) From ca65023eab679b09665dde7dbc14a0dcae52b6f1 Mon Sep 17 00:00:00 2001 From: neuecc Date: Sat, 1 Jun 2024 23:01:12 +0900 Subject: [PATCH 45/54] filter ConsoleAppContext --- ReadMe.md | 10 +- .../Commands/ConsoleAppFrameworkCommand.cs | 14 +- sandbox/GeneratorSandbox/Filters.cs | 76 ++++++++++ sandbox/GeneratorSandbox/Program.cs | 142 +++--------------- src/ConsoleAppFramework/Command.cs | 2 +- .../ConsoleAppGenerator.cs | 8 +- src/ConsoleAppFramework/Emitter.cs | 8 +- .../FilterTest.cs | 46 +++--- 8 files changed, 148 insertions(+), 158 deletions(-) create mode 100644 sandbox/GeneratorSandbox/Filters.cs diff --git a/ReadMe.md b/ReadMe.md index cf730b8..92284aa 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -2,7 +2,7 @@ ConsoleAppFramework === [![GitHub Actions](https://github.com/Cysharp/ConsoleAppFramework/workflows/Build-Debug/badge.svg)](https://github.com/Cysharp/ConsoleAppFramework/actions) [![Releases](https://img.shields.io/github/release/Cysharp/ConsoleAppFramework.svg)](https://github.com/Cysharp/ConsoleAppFramework/releases) -ConsoleAppFramework v5 is Zero Dependency, Zero Overhead, Zero Reflection, Zero Allocation, AOT Safe CLI Framework powered by C# Source Generator. Leveraging the latest features of .NET 8 and C# 12 ([IncrementalGenerator](https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md), [managed function pointer](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-9.0/function-pointers#function-pointers-1), [params arrays and default values lambda expression](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/lambda-expressions#input-parameters-of-a-lambda-expression), [`ISpanParsable`](https://learn.microsoft.com/en-us/dotnet/api/system.ispanparsable-1), [`PosixSignalRegistration`](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.posixsignalregistration), etc.), this library ensures maximum performance while maintaining flexibility and extensibility. +ConsoleAppFramework v5 is Zero Dependency, Zero Overhead, Zero Reflection, Zero Allocation, AOT Safe CLI Framework powered by C# Source Generator; achieves exceptionally high performance and minimal binary size. Leveraging the latest features of .NET 8 and C# 12 ([IncrementalGenerator](https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md), [managed function pointer](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-9.0/function-pointers#function-pointers-1), [params arrays and default values lambda expression](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/lambda-expressions#input-parameters-of-a-lambda-expression), [`ISpanParsable`](https://learn.microsoft.com/en-us/dotnet/api/system.ispanparsable-1), [`PosixSignalRegistration`](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.posixsignalregistration), etc.), this library ensures maximum performance while maintaining flexibility and extensibility. ![image](https://github.com/Cysharp/ConsoleAppFramework/assets/46207/db4bf599-9fe0-4ce4-801f-0003f44d5628) > Set `RunStrategy=ColdStart WarmupCount=0` to calculate the cold start benchmark, which is suitable for CLI application. @@ -118,12 +118,15 @@ ConsoleAppFramework offers a rich set of features as a framework. The Source Gen * Setting option aliases and descriptions from code document comment * `System.ComponentModel.DataAnnotations` attribute-based Validation * Dependency Injection for command registration by type and public methods +* Microsoft.Extensions(Logging, Configuration, etc...) integration * High performance value parsing via `ISpanParsable` * Parsing of params arrays * Parsing of JSON arguments * Help(`-h|--help`) option builder * Default show version(`--version`) option +As you can see from the generated output, the help display is also fast. In typical frameworks, the help string is constructed after the help invocation. However, in ConsoleAppFramework, the help is embedded as string constants, achieving the absolute maximum performance that cannot be surpassed! + Getting Started -- This library is distributed via NuGet, minimal requirement is .NET 8 and C# 12. @@ -330,6 +333,9 @@ app.Run(args); Parse and Value Binding --- + + + // TODO:reason and policy of limitation of parsing `[Argument]` @@ -340,7 +346,7 @@ Parse and Value Binding - + `enum` diff --git a/sandbox/CliFrameworkBenchmark/Commands/ConsoleAppFrameworkCommand.cs b/sandbox/CliFrameworkBenchmark/Commands/ConsoleAppFrameworkCommand.cs index c39613e..8a5fb9a 100644 --- a/sandbox/CliFrameworkBenchmark/Commands/ConsoleAppFrameworkCommand.cs +++ b/sandbox/CliFrameworkBenchmark/Commands/ConsoleAppFrameworkCommand.cs @@ -45,10 +45,10 @@ public static void Execute(string? str, int intOption, bool boolOption, Cancella } } -internal class NopConsoleAppFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) -{ - public override Task InvokeAsync(CancellationToken cancellationToken) - { - return Next.InvokeAsync(cancellationToken); - } -} \ No newline at end of file +//internal class NopConsoleAppFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) +//{ +// public override Task InvokeAsync(CancellationToken cancellationToken) +// { +// return Next.InvokeAsync(cancellationToken); +// } +//} \ No newline at end of file diff --git a/sandbox/GeneratorSandbox/Filters.cs b/sandbox/GeneratorSandbox/Filters.cs new file mode 100644 index 0000000..a5c6b8c --- /dev/null +++ b/sandbox/GeneratorSandbox/Filters.cs @@ -0,0 +1,76 @@ +using ConsoleAppFramework; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace GeneratorSandbox; + + + + + +internal class NopFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) +{ + public override Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) + { + return Next.InvokeAsync(context, cancellationToken); + } +} + + +internal class LogRunningTimeFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) +{ + public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) + { + var startTime = Stopwatch.GetTimestamp(); + ConsoleApp.Log($"Execute command at {DateTime.UtcNow.ToLocalTime()}"); // LocalTime for human readable time + try + { + await Next.InvokeAsync(context, cancellationToken); + ConsoleApp.Log($"Command execute successfully at {DateTime.UtcNow.ToLocalTime()}, Elapsed: " + (Stopwatch.GetElapsedTime(startTime))); + } + catch + { + ConsoleApp.Log($"Command execute failed at {DateTime.UtcNow.ToLocalTime()}, Elapsed: " + (Stopwatch.GetElapsedTime(startTime))); + throw; + } + } +} + + +internal class ChangeExitCodeFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) +{ + public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) + { + try + { + await Next.InvokeAsync(context, cancellationToken); + } + catch (Exception ex) + { + if (ex is OperationCanceledException) return; + + Environment.ExitCode = 9999; // change custom exit code + ConsoleApp.LogError(ex.ToString()); + } + } +} + +internal class MutexFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) +{ + public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) + { + // var name = context.MethodInfo.DeclaringType.Name + "." + context.MethodInfo.Name; + // using (var mutex = new Mutex(true, name, out var createdNew)) ; + + //if (!createdNew) + //{ + // throw new Exception($"already running {name} in another process."); + //} + + await Next.InvokeAsync(context, cancellationToken); + } +} \ No newline at end of file diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index 3e7125c..13ad4a9 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -1,137 +1,43 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.Diagnostics; -using System.Numerics; -using System.Runtime.CompilerServices; -using ConsoleAppFramework; +using ConsoleAppFramework; -//args = ["--foo", "10", "--bar", "20"]; -args = ["--help"]; +var serviceCollection = new MiniDI(); +serviceCollection.Register(typeof(string), "hoge!"); +serviceCollection.Register(typeof(int), 9999); +ConsoleApp.ServiceProvider = serviceCollection; -var app = ConsoleApp.Create(); +var builder = ConsoleApp.Create(); +builder.UseFilter(); -app.Add("foo"); +builder.Add("", () => Console.Write("do")); -// Commands: -// foo -// foo bar -// foo bar barbaz -// foo baz -app.Run(args); +builder.Run(args); -public class MyCommands : IDisposable -{ - /// Root command test. - /// -m, Message to show. - [Command("")] - public void Root(string msg) => Console.WriteLine(msg); - - /// Display message. - /// Message to show. - public void Echo(string msg) => Console.WriteLine(msg); - - /// Sum parameters. - /// left value. - /// right value. - public void Sum(int x, int y) => Console.WriteLine(x + y); - - public void Dispose() - { - Console.WriteLine("Disposed."); - } -} - - -[AttributeUsage(AttributeTargets.Parameter)] -public class Vector3ParserAttribute : Attribute, IArgumentParser -{ - public static bool TryParse(ReadOnlySpan s, out Vector3 result) - { - Span ranges = stackalloc Range[3]; - var splitCount = s.Split(ranges, ','); - if (splitCount != 3) - { - result = default; - return false; - } - - float x; - float y; - float z; - if (float.TryParse(s[ranges[0]], out x) && float.TryParse(s[ranges[1]], out y) && float.TryParse(s[ranges[2]], out z)) - { - result = new Vector3(x, y, z); - return true; - } - - result = default; - return false; - } -} - - - -public class FilterContext : IServiceProvider -{ - public long Timestamp { get; set; } - public Guid UserId { get; set; } - - object IServiceProvider.GetService(Type serviceType) - { - if (serviceType == typeof(FilterContext)) return this; - throw new InvalidOperationException("Type is invalid:" + serviceType); - } -} - -internal class TimestampFilter(ConsoleAppFilter next) +internal class DIFilter(string foo, int bar, ConsoleAppFilter next) : ConsoleAppFilter(next) { - public override Task InvokeAsync(CancellationToken cancellationToken) + public override Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) { - Console.WriteLine("filter1"); - return Next.InvokeAsync(cancellationToken); - } -} + var newContext = context with { State = 100 }; - -internal class LogExecutionTimeFilter(ConsoleAppFilter next) - : ConsoleAppFilter(next) -{ - public override async Task InvokeAsync(CancellationToken cancellationToken) - { - var startingTime = Stopwatch.GetTimestamp(); - try - { - await Next.InvokeAsync(cancellationToken); - } - finally - { - var elapsed = Stopwatch.GetElapsedTime(startingTime); - ConsoleApp.Log($"Execution Time: {elapsed}"); - } + Console.Write("invoke:"); + Console.Write(foo); + Console.Write(bar); + return Next.InvokeAsync(newContext, cancellationToken); } } -internal class NanimosinaiFilter(ConsoleAppFilter next) - : ConsoleAppFilter(next) +public class MiniDI : IServiceProvider { - public override Task InvokeAsync(CancellationToken cancellationToken) + System.Collections.Generic.Dictionary dict = new(); + + public void Register(Type type, object instance) { - Console.WriteLine("filter0"); - return Next.InvokeAsync(cancellationToken); + dict[type] = instance; } -} - -public class MyContext : IServiceProvider -{ - public long Timestamp { get; set; } - public Guid UserId { get; set; } - - object IServiceProvider.GetService(Type serviceType) + public object? GetService(Type serviceType) { - if (serviceType == typeof(MyContext)) return this; - throw new InvalidOperationException("Type is invalid:" + serviceType); + return dict.TryGetValue(serviceType, out var instance) ? instance : null; } -} +} \ No newline at end of file diff --git a/src/ConsoleAppFramework/Command.cs b/src/ConsoleAppFramework/Command.cs index dd76172..3e34c70 100644 --- a/src/ConsoleAppFramework/Command.cs +++ b/src/ConsoleAppFramework/Command.cs @@ -23,7 +23,7 @@ public record class Command { public required bool IsAsync { get; init; } // Task or Task public required bool IsVoid { get; init; } // void or int - public string CommandFullName => (CommandPath.Length == 0) ? CommandName : $"{string.Join("/", CommandPath)}/{CommandName}"; + public string CommandFullName => (CommandPath.Length == 0) ? CommandName : $"{string.Join("/", CommandPath)}/{CommandName}"; // TODO:Join " " public bool IsRootCommand => CommandFullName == ""; public required string[] CommandPath { get; init; } public required string CommandName { get; init; } diff --git a/src/ConsoleAppFramework/ConsoleAppGenerator.cs b/src/ConsoleAppFramework/ConsoleAppGenerator.cs index d1d45e3..261dce6 100644 --- a/src/ConsoleAppFramework/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework/ConsoleAppGenerator.cs @@ -116,11 +116,13 @@ public CommandAttribute(string command) } } +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(CancellationToken cancellationToken); + public abstract Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken); } [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = false)] @@ -313,12 +315,12 @@ static void ShowVersion() static partial void ShowHelp(int helpId); - static async Task RunWithFilterAsync(ConsoleAppFilter invoker) + static async Task RunWithFilterAsync(string commandName, string[] args, ConsoleAppFilter invoker) { using var posixSignalHandler = PosixSignalHandler.Register(Timeout); try { - await Task.Run(() => invoker.InvokeAsync(posixSignalHandler.Token)).WaitAsync(posixSignalHandler.TimeoutToken); + await Task.Run(() => invoker.InvokeAsync(new ConsoleAppContext(commandName, args, null), posixSignalHandler.Token)).WaitAsync(posixSignalHandler.TimeoutToken); } catch (Exception ex) { diff --git a/src/ConsoleAppFramework/Emitter.cs b/src/ConsoleAppFramework/Emitter.cs index 095db64..e4d6c26 100644 --- a/src/ConsoleAppFramework/Emitter.cs +++ b/src/ConsoleAppFramework/Emitter.cs @@ -39,7 +39,7 @@ public void EmitRun(SourceBuilder sb, CommandWithId commandWithId, bool isRunAsy sb.AppendLine(); } - var filterCancellationToken = command.HasFilter ? ", CancellationToken cancellationToken" : ""; + var filterCancellationToken = command.HasFilter ? ", ConsoleAppContext context, CancellationToken cancellationToken" : ""; // method signature using (sb.BeginBlock($"{accessibility} static {unsafeCode}{returnType} {methodName}({argsType} args{commandMethodType}{filterCancellationToken})")) @@ -486,7 +486,7 @@ void EmitLeafCommand(CommandWithId? command) } else { - var invokeCode = $"RunWithFilterAsync(new Command{command.Id}Invoker(args[{depth}..]{commandArgs}).BuildFilter())"; + var invokeCode = $"RunWithFilterAsync(\"{command.Command.CommandFullName}\", args, new Command{command.Id}Invoker(args[{depth}..]{commandArgs}).BuildFilter())"; if (!isRunAsync) { sb.AppendLine($"{invokeCode}.GetAwaiter().GetResult();"); @@ -520,10 +520,10 @@ void EmitFilterInvoker(CommandWithId command) } sb.AppendLine(); - using (sb.BeginBlock($"public override Task InvokeAsync(CancellationToken cancellationToken)")) + using (sb.BeginBlock($"public override Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken)")) { var cmdArgs = needsCommand ? ", command" : ""; - sb.AppendLine($"return RunCommand{command.Id}Async(args{cmdArgs}, cancellationToken);"); + sb.AppendLine($"return RunCommand{command.Id}Async(args{cmdArgs}, context, cancellationToken);"); } } } diff --git a/tests/ConsoleAppFramework.GeneratorTests/FilterTest.cs b/tests/ConsoleAppFramework.GeneratorTests/FilterTest.cs index c650a03..46cbcfa 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/FilterTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/FilterTest.cs @@ -34,40 +34,40 @@ void Hello() internal class NopFilter1(ConsoleAppFilter next) : ConsoleAppFilter(next) { - public override Task InvokeAsync(CancellationToken cancellationToken) + public override Task InvokeAsync(ConsoleAppContext context,CancellationToken cancellationToken) { Console.Write(1); - return Next.InvokeAsync(cancellationToken); + return Next.InvokeAsync(context, cancellationToken); } } internal class NopFilter2(ConsoleAppFilter next) : ConsoleAppFilter(next) { - public override Task InvokeAsync(CancellationToken cancellationToken) + public override Task InvokeAsync(ConsoleAppContext context,CancellationToken cancellationToken) { Console.Write(2); - return Next.InvokeAsync(cancellationToken); + return Next.InvokeAsync(context, cancellationToken); } } internal class NopFilter3(ConsoleAppFilter next) : ConsoleAppFilter(next) { - public override Task InvokeAsync(CancellationToken cancellationToken) + public override Task InvokeAsync(ConsoleAppContext context,CancellationToken cancellationToken) { Console.Write(3); - return Next.InvokeAsync(cancellationToken); + return Next.InvokeAsync(context, cancellationToken); } } internal class NopFilter4(ConsoleAppFilter next) : ConsoleAppFilter(next) { - public override Task InvokeAsync(CancellationToken cancellationToken) + public override Task InvokeAsync(ConsoleAppContext context,CancellationToken cancellationToken) { Console.Write(4); - return Next.InvokeAsync(cancellationToken); + return Next.InvokeAsync(context, cancellationToken); } } """, args: "", expected: "1234abcde"); @@ -101,60 +101,60 @@ public void Hello() internal class NopFilter1(ConsoleAppFilter next) : ConsoleAppFilter(next) { - public override Task InvokeAsync(CancellationToken cancellationToken) + public override Task InvokeAsync(ConsoleAppContext context,CancellationToken cancellationToken) { Console.Write(1); - return Next.InvokeAsync(cancellationToken); + return Next.InvokeAsync(context, cancellationToken); } } internal class NopFilter2(ConsoleAppFilter next) : ConsoleAppFilter(next) { - public override Task InvokeAsync(CancellationToken cancellationToken) + public override Task InvokeAsync(ConsoleAppContext context,CancellationToken cancellationToken) { Console.Write(2); - return Next.InvokeAsync(cancellationToken); + return Next.InvokeAsync(context, cancellationToken); } } internal class NopFilter3(ConsoleAppFilter next) : ConsoleAppFilter(next) { - public override Task InvokeAsync(CancellationToken cancellationToken) + public override Task InvokeAsync(ConsoleAppContext context,CancellationToken cancellationToken) { Console.Write(3); - return Next.InvokeAsync(cancellationToken); + return Next.InvokeAsync(context, cancellationToken); } } internal class NopFilter4(ConsoleAppFilter next) : ConsoleAppFilter(next) { - public override Task InvokeAsync(CancellationToken cancellationToken) + public override Task InvokeAsync(ConsoleAppContext context,CancellationToken cancellationToken) { Console.Write(4); - return Next.InvokeAsync(cancellationToken); + return Next.InvokeAsync(context, cancellationToken); } } internal class NopFilter5(ConsoleAppFilter next) : ConsoleAppFilter(next) { - public override Task InvokeAsync(CancellationToken cancellationToken) + public override Task InvokeAsync(ConsoleAppContext context,CancellationToken cancellationToken) { Console.Write(5); - return Next.InvokeAsync(cancellationToken); + return Next.InvokeAsync(context, cancellationToken); } } internal class NopFilter6(ConsoleAppFilter next) : ConsoleAppFilter(next) { - public override Task InvokeAsync(CancellationToken cancellationToken) + public override Task InvokeAsync(ConsoleAppContext context,CancellationToken cancellationToken) { Console.Write(6); - return Next.InvokeAsync(cancellationToken); + return Next.InvokeAsync(context, cancellationToken); } } """, args: "hello", expected: "123456abcde"); @@ -181,12 +181,12 @@ public void DI() internal class DIFilter(string foo, int bar, ConsoleAppFilter next) : ConsoleAppFilter(next) { - public override Task InvokeAsync(CancellationToken cancellationToken) + public override Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) { Console.Write("invoke:"); Console.Write(foo); Console.Write(bar); - return Next.InvokeAsync(cancellationToken); + return Next.InvokeAsync(context, cancellationToken); } } @@ -199,7 +199,7 @@ public void Register(Type type, object instance) dict[type] = instance; } - public object GetService(Type serviceType) + public object? GetService(Type serviceType) { return dict.TryGetValue(serviceType, out var instance) ? instance : null; } From 3367c87b731ca84541d6d35d61ce326b88a9d2c4 Mon Sep 17 00:00:00 2001 From: neuecc Date: Sat, 1 Jun 2024 23:35:59 +0900 Subject: [PATCH 46/54] ConsoleAppContext for Command --- sandbox/GeneratorSandbox/Program.cs | 47 ++++++++++++++++++++++++++++- src/ConsoleAppFramework/Command.cs | 3 +- src/ConsoleAppFramework/Emitter.cs | 18 ++++++++--- src/ConsoleAppFramework/Parser.cs | 6 +++- 4 files changed, 67 insertions(+), 7 deletions(-) diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index 13ad4a9..2360981 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -1,4 +1,5 @@ using ConsoleAppFramework; +using System.ComponentModel.DataAnnotations; var serviceCollection = new MiniDI(); serviceCollection.Register(typeof(string), "hoge!"); @@ -9,10 +10,13 @@ builder.UseFilter(); -builder.Add("", () => Console.Write("do")); +builder.Add("", (ConsoleAppContext ctx) => Console.Write("do")); builder.Run(args); + + + internal class DIFilter(string foo, int bar, ConsoleAppFilter next) : ConsoleAppFilter(next) { @@ -40,4 +44,45 @@ public void Register(Type type, object instance) { return dict.TryGetValue(serviceType, out var instance) ? instance : null; } +} + +namespace ConsoleAppFramework +{ + partial class ConsoleApp + { + static async Task RunWithFilterAsync2(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); + + + await Task.Factory.StartNew(static state => Task.CompletedTask, 1983, default, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default).Unwrap(); + + + + } + catch (Exception ex) + { + if (ex is OperationCanceledException) + { + Environment.ExitCode = 130; + return; + } + + Environment.ExitCode = 1; + if (ex is ValidationException) + { + LogError(ex.Message); + } + else + { + LogError(ex.ToString()); + } + } + } + } } \ No newline at end of file diff --git a/src/ConsoleAppFramework/Command.cs b/src/ConsoleAppFramework/Command.cs index 3e34c70..90e7492 100644 --- a/src/ConsoleAppFramework/Command.cs +++ b/src/ConsoleAppFramework/Command.cs @@ -157,8 +157,9 @@ public record class CommandParameter public object? DefaultValue { get; init; } public required ITypeSymbol? CustomParserType { get; init; } public required bool IsFromServices { get; init; } + public required bool IsConsoleAppContext { get; init; } public required bool IsCancellationToken { get; init; } - public bool IsParsable => !(IsFromServices || IsCancellationToken); + public bool IsParsable => !(IsFromServices || IsCancellationToken || IsConsoleAppContext); public bool IsFlag => Type.SpecialType == SpecialType.System_Boolean; public required bool HasValidation { get; init; } public required int ArgumentIndex { get; init; } // -1 is not Argument, other than marked as [Argument] diff --git a/src/ConsoleAppFramework/Emitter.cs b/src/ConsoleAppFramework/Emitter.cs index e4d6c26..f3a4bd2 100644 --- a/src/ConsoleAppFramework/Emitter.cs +++ b/src/ConsoleAppFramework/Emitter.cs @@ -11,6 +11,7 @@ public void EmitRun(SourceBuilder sb, CommandWithId commandWithId, bool isRunAsy var emitForBuilder = methodName != null; var hasCancellationToken = command.Parameters.Any(x => x.IsCancellationToken); + var hasConsoleAppContext = command.Parameters.Any(x => x.IsConsoleAppContext); var hasArgument = command.Parameters.Any(x => x.IsArgument); var hasValidation = command.Parameters.Any(x => x.HasValidation); var parsableParameterCount = command.Parameters.Count(x => x.IsParsable); @@ -19,6 +20,7 @@ public void EmitRun(SourceBuilder sb, CommandWithId commandWithId, bool isRunAsy { isRunAsync = true; hasCancellationToken = false; + hasConsoleAppContext = false; } var returnType = isRunAsync ? "async Task" : "void"; var accessibility = !emitForBuilder ? "public" : "private"; @@ -42,7 +44,7 @@ public void EmitRun(SourceBuilder sb, CommandWithId commandWithId, bool isRunAsy var filterCancellationToken = command.HasFilter ? ", ConsoleAppContext context, CancellationToken cancellationToken" : ""; // method signature - using (sb.BeginBlock($"{accessibility} static {unsafeCode}{returnType} {methodName}({argsType} args{commandMethodType}{filterCancellationToken})")) + using (sb.BeginBlock($"{accessibility} static {unsafeCode}{returnType} {methodName}(string[] rawArgs, {argsType} args{commandMethodType}{filterCancellationToken})")) { sb.AppendLine($"if (TryShowHelpOrVersion(args, {parsableParameterCount}, {commandWithId.Id})) return;"); sb.AppendLine(); @@ -52,6 +54,10 @@ public void EmitRun(SourceBuilder sb, CommandWithId commandWithId, bool isRunAsy { sb.AppendLine("using var posixSignalHandler = PosixSignalHandler.Register(Timeout);"); } + if (hasConsoleAppContext) + { + sb.AppendLine($"var context = new ConsoleAppContext(\"{command.CommandFullName}\", rawArgs, null);"); + } for (var i = 0; i < command.Parameters.Length; i++) { var parameter = command.Parameters[i]; @@ -77,6 +83,10 @@ public void EmitRun(SourceBuilder sb, CommandWithId commandWithId, bool isRunAsy sb.AppendLine($"var arg{i} = posixSignalHandler.Token;"); } } + else if (parameter.IsConsoleAppContext) + { + sb.AppendLine($"var arg{i} = context;"); + } else if (parameter.IsFromServices) { var type = parameter.Type.ToFullyQualifiedFormatDisplayString(); @@ -477,11 +487,11 @@ void EmitLeafCommand(CommandWithId? command) { if (!isRunAsync) { - sb.AppendLine($"RunCommand{command.Id}(args.AsSpan({depth}){commandArgs});"); + sb.AppendLine($"RunCommand{command.Id}(args, args.AsSpan({depth}){commandArgs});"); } else { - sb.AppendLine($"result = RunCommand{command.Id}Async(args[{depth}..]{commandArgs});"); + sb.AppendLine($"result = RunCommand{command.Id}Async(args, args[{depth}..]{commandArgs});"); } } else @@ -523,7 +533,7 @@ void EmitFilterInvoker(CommandWithId command) using (sb.BeginBlock($"public override Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken)")) { var cmdArgs = needsCommand ? ", command" : ""; - sb.AppendLine($"return RunCommand{command.Id}Async(args{cmdArgs}, context, cancellationToken);"); + sb.AppendLine($"return RunCommand{command.Id}Async(context.Arguments, args{cmdArgs}, context, cancellationToken);"); } } } diff --git a/src/ConsoleAppFramework/Parser.cs b/src/ConsoleAppFramework/Parser.cs index bfee114..9c86d37 100644 --- a/src/ConsoleAppFramework/Parser.cs +++ b/src/ConsoleAppFramework/Parser.cs @@ -332,9 +332,10 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta }); var isCancellationToken = SymbolEqualityComparer.Default.Equals(type.Type!, wellKnownTypes.CancellationToken); + var isConsoleAppContext = type.Type!.Name == "ConsoleAppContext"; var argumentIndex = -1; - if (!(isFromServices || isCancellationToken)) + if (!(isFromServices || isCancellationToken || isConsoleAppContext)) { if (hasArgument) { @@ -353,6 +354,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta Name = NameConverter.ToKebabCase(x.Identifier.Text), OriginalParameterName = x.Identifier.Text, IsNullableReference = isNullableReference, + IsConsoleAppContext = isConsoleAppContext, IsParams = hasParams, Type = type.Type!, Location = x.GetLocation(), @@ -479,6 +481,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta var hasArgument = x.GetAttributes().Any(x => x.AttributeClass?.Name == "ArgumentAttribute"); var hasValidation = x.GetAttributes().Any(x => x.AttributeClass?.GetBaseTypes().Any(y => y.Name == "ValidationAttribute") ?? false); var isCancellationToken = SymbolEqualityComparer.Default.Equals(x.Type, wellKnownTypes.CancellationToken); + var isConsoleAppContext = x.Type!.Name == "ConsoleAppContext"; string description = ""; string[] aliases = []; @@ -507,6 +510,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta Name = NameConverter.ToKebabCase(x.Name), OriginalParameterName = x.Name, IsNullableReference = isNullableReference, + IsConsoleAppContext = isConsoleAppContext, IsParams = x.IsParams, Location = x.DeclaringSyntaxReferences[0].GetSyntax().GetLocation(), Type = x.Type, From 3682ad3bedebd641f09d6c1d777d566deeb08da3 Mon Sep 17 00:00:00 2001 From: neuecc Date: Sun, 2 Jun 2024 00:34:47 +0900 Subject: [PATCH 47/54] commandpath uses ` ` --- sandbox/CliFrameworkBenchmark/Benchmark.cs | 1 + src/ConsoleAppFramework/Command.cs | 8 +- src/ConsoleAppFramework/CommandHelpBuilder.cs | 19 +--- .../ConsoleAppGenerator.cs | 10 +- src/ConsoleAppFramework/Emitter.cs | 22 ++-- src/ConsoleAppFramework/Parser.cs | 39 +++---- .../HelpTest.cs | 6 +- .../SubCommandTest.cs | 104 +++++++++--------- 8 files changed, 93 insertions(+), 116 deletions(-) diff --git a/sandbox/CliFrameworkBenchmark/Benchmark.cs b/sandbox/CliFrameworkBenchmark/Benchmark.cs index fdae49b..0a7fc0c 100644 --- a/sandbox/CliFrameworkBenchmark/Benchmark.cs +++ b/sandbox/CliFrameworkBenchmark/Benchmark.cs @@ -7,6 +7,7 @@ using CliFx; using Cocona.Benchmark.External.Commands; using CommandLine; +using ConsoleAppFramework; using PowerArgs; using Spectre.Console.Cli; using System.ComponentModel.DataAnnotations.Schema; diff --git a/src/ConsoleAppFramework/Command.cs b/src/ConsoleAppFramework/Command.cs index 90e7492..94a26e0 100644 --- a/src/ConsoleAppFramework/Command.cs +++ b/src/ConsoleAppFramework/Command.cs @@ -23,10 +23,10 @@ public record class Command { public required bool IsAsync { get; init; } // Task or Task public required bool IsVoid { get; init; } // void or int - public string CommandFullName => (CommandPath.Length == 0) ? CommandName : $"{string.Join("/", CommandPath)}/{CommandName}"; // TODO:Join " " - public bool IsRootCommand => CommandFullName == ""; - public required string[] CommandPath { get; init; } - public required string CommandName { get; init; } + + public bool IsRootCommand => Name == ""; + public required string Name { get; init; } + public required CommandParameter[] Parameters { get; init; } public required string Description { get; init; } public required MethodKind MethodKind { get; init; } diff --git a/src/ConsoleAppFramework/CommandHelpBuilder.cs b/src/ConsoleAppFramework/CommandHelpBuilder.cs index cf4e72d..0e7a73e 100644 --- a/src/ConsoleAppFramework/CommandHelpBuilder.cs +++ b/src/ConsoleAppFramework/CommandHelpBuilder.cs @@ -34,7 +34,7 @@ public static string BuildRootHelpMessage(Command[] commands) if (withoutRoot.Length == 0) return sb.ToString(); - var helpDefinitions = withoutRoot.OrderBy(x => x.CommandFullName).ToArray(); + var helpDefinitions = withoutRoot.OrderBy(x => x.Name).ToArray(); var list = BuildMethodListMessage(helpDefinitions, out _); sb.Append(list); @@ -44,7 +44,7 @@ public static string BuildRootHelpMessage(Command[] commands) public static string BuildCommandHelpMessage(Command command) { - return BuildHelpMessageCore(command, showCommandName: command.CommandName != "", showCommand: false); + return BuildHelpMessageCore(command, showCommandName: command.Name != "", showCommand: false); } static string BuildHelpMessageCore(Command command, bool showCommandName, bool showCommand) @@ -217,13 +217,7 @@ static string BuildMethodListMessage(IEnumerable commands, out int maxW var formatted = commands .Select(x => { - var full = x.CommandName; - if (x.CommandPath.Length > 0) - { - full = string.Join(" ", x.CommandPath) + " " + x.CommandName; - } - - return (Command: full, x.Description); + return (Command: x.Name, x.Description); }) .ToArray(); maxWidth = formatted.Max(x => x.Command.Length); @@ -309,12 +303,7 @@ static CommandHelpDefinition CreateCommandHelpDefinition(Command descriptor) parameterDefinitions.Add(new CommandOptionHelpDefinition(options.Distinct().ToArray(), description, paramTypeName, defaultValue, index, isFlag, isParams)); } - var commandName = descriptor.CommandName; - if (descriptor.CommandPath.Length != 0) - { - commandName = string.Join(" ", descriptor.CommandPath) + " " + descriptor.CommandName; - } - + var commandName = descriptor.Name; return new CommandHelpDefinition( commandName, parameterDefinitions.ToArray(), diff --git a/src/ConsoleAppFramework/ConsoleAppGenerator.cs b/src/ConsoleAppFramework/ConsoleAppGenerator.cs index 261dce6..fed14f6 100644 --- a/src/ConsoleAppFramework/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework/ConsoleAppGenerator.cs @@ -613,9 +613,9 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex var command = parser.ParseAndValidateForBuilderDelegateRegistration(); // validation command name duplicate - if (command != null && !names.Add(command.CommandFullName)) + if (command != null && !names.Add(command.Name)) { - sourceProductionContext.ReportDiagnostic(DiagnosticDescriptors.DuplicateCommandName, x.Node.ArgumentList.Arguments[0].GetLocation(), command!.CommandFullName); + sourceProductionContext.ReportDiagnostic(DiagnosticDescriptors.DuplicateCommandName, x.Node.ArgumentList.Arguments[0].GetLocation(), command!.Name); return null; } @@ -629,12 +629,12 @@ static void EmitConsoleAppBuilder(SourceProductionContext sourceProductionContex var parser = new Parser(sourceProductionContext, x.Node, x.Model, wellKnownTypes, DelegateBuildType.None, globalFilters); var commands = parser.ParseAndValidateForBuilderClassRegistration(); - // validation command name duplicate? + // validation command name duplicate foreach (var command in commands) { - if (command != null && !names.Add(command.CommandFullName)) + if (command != null && !names.Add(command.Name)) { - sourceProductionContext.ReportDiagnostic(DiagnosticDescriptors.DuplicateCommandName, x.Node.GetLocation(), command!.CommandFullName); + sourceProductionContext.ReportDiagnostic(DiagnosticDescriptors.DuplicateCommandName, x.Node.GetLocation(), command!.Name); return [null]; } } diff --git a/src/ConsoleAppFramework/Emitter.cs b/src/ConsoleAppFramework/Emitter.cs index f3a4bd2..d665859 100644 --- a/src/ConsoleAppFramework/Emitter.cs +++ b/src/ConsoleAppFramework/Emitter.cs @@ -42,9 +42,10 @@ public void EmitRun(SourceBuilder sb, CommandWithId commandWithId, bool isRunAsy } var filterCancellationToken = command.HasFilter ? ", ConsoleAppContext context, CancellationToken cancellationToken" : ""; + var rawArgs = !emitForBuilder ? "" : "string[] rawArgs, "; // method signature - using (sb.BeginBlock($"{accessibility} static {unsafeCode}{returnType} {methodName}(string[] rawArgs, {argsType} args{commandMethodType}{filterCancellationToken})")) + using (sb.BeginBlock($"{accessibility} static {unsafeCode}{returnType} {methodName}({rawArgs}{argsType} args{commandMethodType}{filterCancellationToken})")) { sb.AppendLine($"if (TryShowHelpOrVersion(args, {parsableParameterCount}, {commandWithId.Id})) return;"); sb.AppendLine(); @@ -56,7 +57,8 @@ public void EmitRun(SourceBuilder sb, CommandWithId commandWithId, bool isRunAsy } if (hasConsoleAppContext) { - sb.AppendLine($"var context = new ConsoleAppContext(\"{command.CommandFullName}\", rawArgs, null);"); + var rawArgsName = !emitForBuilder ? "args" : "rawArgs"; + sb.AppendLine($"var context = new ConsoleAppContext(\"{command.Name}\", {rawArgsName}, null);"); } for (var i = 0; i < command.Parameters.Length; i++) { @@ -300,7 +302,7 @@ public void EmitRun(SourceBuilder sb, CommandWithId commandWithId, bool isRunAsy public void EmitBuilder(SourceBuilder sb, CommandWithId[] commandIds, bool emitSync, bool emitAsync) { // grouped by path - var commandGroup = commandIds.ToLookup(x => x.Command.CommandPath.Length == 0 ? x.Command.CommandName : x.Command.CommandPath[0]); + var commandGroup = commandIds.ToLookup(x => x.Command.Name.Split(' ')[0]); var hasRootCommand = commandIds.Any(x => x.Command.IsRootCommand); using (sb.BeginBlock("partial struct ConsoleAppBuilder")) @@ -319,7 +321,7 @@ public void EmitBuilder(SourceBuilder sb, CommandWithId[] commandIds, bool emitS { foreach (var item in commandIds.Where(x => x.FieldType != null)) { - using (sb.BeginIndent($"case \"{item.Command.CommandFullName}\":")) + using (sb.BeginIndent($"case \"{item.Command.Name}\":")) { sb.AppendLine($"this.command{item.Id} = Unsafe.As<{item.FieldType}>(command);"); sb.AppendLine("break;"); @@ -441,14 +443,10 @@ void EmitRunBody(ILookup groupedCommands, int depth, bool var nextGroup = commands .ToLookup(x => { - var len = x.Command.CommandPath.Length; - if (len > nextDepth) + var path = x.Command.Name.Split(' '); + if (path.Length > nextDepth) { - return x.Command.CommandPath[nextDepth]; - } - if (len == nextDepth) - { - return x.Command.CommandName; + return path[nextDepth]; } else { @@ -496,7 +494,7 @@ void EmitLeafCommand(CommandWithId? command) } else { - var invokeCode = $"RunWithFilterAsync(\"{command.Command.CommandFullName}\", args, new Command{command.Id}Invoker(args[{depth}..]{commandArgs}).BuildFilter())"; + var invokeCode = $"RunWithFilterAsync(\"{command.Command.Name}\", args, new Command{command.Id}Invoker(args[{depth}..]{commandArgs}).BuildFilter())"; if (!isRunAsync) { sb.AppendLine($"{invokeCode}.GetAwaiter().GetResult();"); diff --git a/src/ConsoleAppFramework/Parser.cs b/src/ConsoleAppFramework/Parser.cs index 9c86d37..7a58205 100644 --- a/src/ConsoleAppFramework/Parser.cs +++ b/src/ConsoleAppFramework/Parser.cs @@ -1,7 +1,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -using System.Xml.Linq; namespace ConsoleAppFramework; @@ -12,7 +11,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta var args = node.ArgumentList.Arguments; if (args.Count == 2) // 0 = args, 1 = lambda { - var command = ExpressionToCommand(args[1].Expression, [], ""); // rootCommand(path and commandName = "") + var command = ExpressionToCommand(args[1].Expression, ""); // rootCommand(commandName = "") if (command != null) { return ValidateCommand(command); @@ -26,7 +25,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta public Command? ParseAndValidateForBuilderDelegateRegistration() // for ConsoleAppBuilder.Add { - // Add(string commandName) + // Add(string commandName, Delgate command) var args = node.ArgumentList.Arguments; if (args.Count == 2) // 0 = string command, 1 = lambda { @@ -38,15 +37,8 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta return null; } - string[] path = []; var name = (commandName.Expression as LiteralExpressionSyntax)!.Token.ValueText; - var pathAndName = name.Split(['/'], StringSplitOptions.RemoveEmptyEntries); - if (pathAndName.Length > 1) - { - path = pathAndName.AsSpan(0, pathAndName.Length - 1).ToArray(); - name = pathAndName[^1]; - } - var command = ExpressionToCommand(args[1].Expression, path, name); + var command = ExpressionToCommand(args[1].Expression, name); if (command != null) { return ValidateCommand(command); @@ -64,7 +56,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta var genericType = genericName!.TypeArgumentList.Arguments[0]; // Add(string commandPath) - string[] commandPath = []; + string? commandPath = null; var args = node.ArgumentList.Arguments; if (node.ArgumentList.Arguments.Count == 1) { @@ -75,8 +67,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta return []; } - var name = (commandName.Expression as LiteralExpressionSyntax)!.Token.ValueText; - commandPath = name.Split(['/'], StringSplitOptions.RemoveEmptyEntries); + commandPath = (commandName.Expression as LiteralExpressionSyntax)!.Token.ValueText; } // T @@ -160,7 +151,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta commandName = NameConverter.ToKebabCase(x.Name); } - var command = ParseFromMethodSymbol(x, false, commandPath, commandName, typeFilters); + var command = ParseFromMethodSymbol(x, false, (commandPath == null) ? commandName : $"{commandPath.Trim()} {commandName}", typeFilters); if (command == null) return null; command.CommandMethodInfo = methodInfoBase with { MethodName = x.Name }; @@ -169,7 +160,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta .ToArray(); } - Command? ExpressionToCommand(ExpressionSyntax expression, string[] commandPath, string commandName) + Command? ExpressionToCommand(ExpressionSyntax expression, string commandName) { var lambda = expression as ParenthesizedLambdaExpressionSyntax; if (lambda == null) @@ -181,7 +172,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta var methodSymbols = model.GetMemberGroup(operand); if (methodSymbols.Length > 0 && methodSymbols[0] is IMethodSymbol methodSymbol) { - return ParseFromMethodSymbol(methodSymbol, addressOf: true, commandPath, commandName, []); + return ParseFromMethodSymbol(methodSymbol, addressOf: true, commandName, []); } } else @@ -189,19 +180,19 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta var methodSymbols = model.GetMemberGroup(expression); if (methodSymbols.Length > 0 && methodSymbols[0] is IMethodSymbol methodSymbol) { - return ParseFromMethodSymbol(methodSymbol, addressOf: false, commandPath, commandName, []); + return ParseFromMethodSymbol(methodSymbol, addressOf: false, commandName, []); } } } else { - return ParseFromLambda(lambda, commandPath, commandName); + return ParseFromLambda(lambda, commandName); } return null; } - Command? ParseFromLambda(ParenthesizedLambdaExpressionSyntax lambda, string[] commandPath, string commandName) + Command? ParseFromLambda(ParenthesizedLambdaExpressionSyntax lambda, string commandName) { var isAsync = lambda.AsyncKeyword.IsKind(SyntaxKind.AsyncKeyword); @@ -374,8 +365,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta var cmd = new Command { - CommandName = commandName, - CommandPath = commandPath, + Name = commandName, IsAsync = isAsync, IsVoid = isVoid, Parameters = parameters, @@ -388,7 +378,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta return cmd; } - Command? ParseFromMethodSymbol(IMethodSymbol methodSymbol, bool addressOf, string[] commandPath, string commandName, FilterInfo[] typeFilters) + Command? ParseFromMethodSymbol(IMethodSymbol methodSymbol, bool addressOf, string commandName, FilterInfo[] typeFilters) { var docComment = methodSymbol.DeclaringSyntaxReferences[0].GetSyntax().GetDocumentationCommentTriviaSyntax(); var summary = ""; @@ -529,8 +519,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta var cmd = new Command { - CommandName = commandName, - CommandPath = commandPath, + Name = commandName, IsAsync = isAsync, IsVoid = isVoid, Parameters = parameters, diff --git a/tests/ConsoleAppFramework.GeneratorTests/HelpTest.cs b/tests/ConsoleAppFramework.GeneratorTests/HelpTest.cs index 3b969c6..55fe21f 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/HelpTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/HelpTest.cs @@ -67,7 +67,7 @@ public void ListWithoutRoot() var app = ConsoleApp.Create(); app.Add("a", (int x, int y) => { }); app.Add("ab", (int x, int y) => { }); -app.Add("a/b/c", (int x, int y) => { }); +app.Add("a b c", (int x, int y) => { }); app.Run(args); """; verifier.Execute(code, args: "--help", expected: """ @@ -89,7 +89,7 @@ public void ListWithRoot() app.Add("", (int x, int y) => { }); app.Add("a", (int x, int y) => { }); app.Add("ab", (int x, int y) => { }); -app.Add("a/b/c", (int x, int y) => { }); +app.Add("a b c", (int x, int y) => { }); app.Run(args); """; verifier.Execute(code, args: "--help", expected: """ @@ -115,7 +115,7 @@ public void SelectLeafHelp() app.Add("", (int x, int y) => { }); app.Add("a", (int x, int y) => { }); app.Add("ab", (int x, int y) => { }); -app.Add("a/b/c", (int x, int y) => { }); +app.Add("a b c", (int x, int y) => { }); app.Run(args); """; verifier.Execute(code, args: "a b c --help", expected: """ diff --git a/tests/ConsoleAppFramework.GeneratorTests/SubCommandTest.cs b/tests/ConsoleAppFramework.GeneratorTests/SubCommandTest.cs index 04fca04..13acb27 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/SubCommandTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/SubCommandTest.cs @@ -19,24 +19,24 @@ public void Zeroargs() builder.Add("", () => { Console.Write("root"); }); builder.Add("a", () => { Console.Write("a"); }); -builder.Add("a/b1", () => { Console.Write("a/b1"); }); -builder.Add("a/b2", () => { Console.Write("a/b2"); }); -builder.Add("a/b2/c", () => { Console.Write("a/b2/c"); }); -builder.Add("a/b2/d", () => { Console.Write("a/b2/d"); }); -builder.Add("a/b2/d/e", () => { Console.Write("a/b2/d/e"); }); -builder.Add("a/b/c/d/e/f", () => { Console.Write("a/b/c/d/e/f"); }); +builder.Add("a b1", () => { Console.Write("a b1"); }); +builder.Add("a b2", () => { Console.Write("a b2"); }); +builder.Add("a b2 c", () => { Console.Write("a b2 c"); }); +builder.Add("a b2 d", () => { Console.Write("a b2 d"); }); +builder.Add("a b2 d e", () => { Console.Write("a b2 d e"); }); +builder.Add("a b c d e f", () => { Console.Write("a b c d e f"); }); builder.Run(args); """; - verifier.Execute(code, "", "root"); // root + verifier.Execute(code, "", "root"); verifier.Execute(code, "a", "a"); - verifier.Execute(code, "a b1", "a/b1"); - verifier.Execute(code, "a b2", "a/b2"); - verifier.Execute(code, "a b2 c", "a/b2/c"); - verifier.Execute(code, "a b2 d", "a/b2/d"); - verifier.Execute(code, "a b2 d e", "a/b2/d/e"); - verifier.Execute(code, "a b c d e f", "a/b/c/d/e/f"); + verifier.Execute(code, "a b1", "a b1"); + verifier.Execute(code, "a b2", "a b2"); + verifier.Execute(code, "a b2 c", "a b2 c"); + verifier.Execute(code, "a b2 d", "a b2 d"); + verifier.Execute(code, "a b2 d e", "a b2 d e"); + verifier.Execute(code, "a b c d e f", "a b c d e f"); } [Fact] @@ -47,24 +47,24 @@ public void Withargs() builder.Add("", (int x, int y) => { Console.Write($"root {x} {y}"); }); builder.Add("a", (int x, int y) => { Console.Write($"a {x} {y}"); }); -builder.Add("a/b1", (int x, int y) => { Console.Write($"a/b1 {x} {y}"); }); -builder.Add("a/b2", (int x, int y) => { Console.Write($"a/b2 {x} {y}"); }); -builder.Add("a/b2/c", (int x, int y) => { Console.Write($"a/b2/c {x} {y}"); }); -builder.Add("a/b2/d", (int x, int y) => { Console.Write($"a/b2/d {x} {y}"); }); -builder.Add("a/b2/d/e", (int x, int y) => { Console.Write($"a/b2/d/e {x} {y}"); }); -builder.Add("a/b/c/d/e/f", (int x, int y) => { Console.Write($"a/b/c/d/e/f {x} {y}"); }); +builder.Add("a b1", (int x, int y) => { Console.Write($"a b1 {x} {y}"); }); +builder.Add("a b2", (int x, int y) => { Console.Write($"a b2 {x} {y}"); }); +builder.Add("a b2 c", (int x, int y) => { Console.Write($"a b2 c {x} {y}"); }); +builder.Add("a b2 d", (int x, int y) => { Console.Write($"a b2 d {x} {y}"); }); +builder.Add("a b2 d e", (int x, int y) => { Console.Write($"a b2 d e {x} {y}"); }); +builder.Add("a b c d e f", (int x, int y) => { Console.Write($"a b c d e f {x} {y}"); }); builder.Run(args); """; - verifier.Execute(code, "--x 10 --y 20", "root 10 20"); // root + verifier.Execute(code, "--x 10 --y 20", "root 10 20"); verifier.Execute(code, "a --x 10 --y 20", "a 10 20"); - verifier.Execute(code, "a b1 --x 10 --y 20", "a/b1 10 20"); - verifier.Execute(code, "a b2 --x 10 --y 20", "a/b2 10 20"); - verifier.Execute(code, "a b2 c --x 10 --y 20", "a/b2/c 10 20"); - verifier.Execute(code, "a b2 d --x 10 --y 20", "a/b2/d 10 20"); - verifier.Execute(code, "a b2 d e --x 10 --y 20", "a/b2/d/e 10 20"); - verifier.Execute(code, "a b c d e f --x 10 --y 20", "a/b/c/d/e/f 10 20"); + verifier.Execute(code, "a b1 --x 10 --y 20", "a b1 10 20"); + verifier.Execute(code, "a b2 --x 10 --y 20", "a b2 10 20"); + verifier.Execute(code, "a b2 c --x 10 --y 20", "a b2 c 10 20"); + verifier.Execute(code, "a b2 d --x 10 --y 20", "a b2 d 10 20"); + verifier.Execute(code, "a b2 d e --x 10 --y 20", "a b2 d e 10 20"); + verifier.Execute(code, "a b c d e f --x 10 --y 20", "a b c d e f 10 20"); } [Fact] @@ -75,24 +75,24 @@ public void ZeroargsAsync() builder.Add("", () => { Console.Write("root"); }); builder.Add("a", () => { Console.Write("a"); }); -builder.Add("a/b1", () => { Console.Write("a/b1"); }); -builder.Add("a/b2", () => { Console.Write("a/b2"); }); -builder.Add("a/b2/c", () => { Console.Write("a/b2/c"); }); -builder.Add("a/b2/d", () => { Console.Write("a/b2/d"); }); -builder.Add("a/b2/d/e", () => { Console.Write("a/b2/d/e"); }); -builder.Add("a/b/c/d/e/f", () => { Console.Write("a/b/c/d/e/f"); }); +builder.Add("a b1", () => { Console.Write("a b1"); }); +builder.Add("a b2", () => { Console.Write("a b2"); }); +builder.Add("a b2 c", () => { Console.Write("a b2 c"); }); +builder.Add("a b2 d", () => { Console.Write("a b2 d"); }); +builder.Add("a b2 d e", () => { Console.Write("a b2 d e"); }); +builder.Add("a b c d e f", () => { Console.Write("a b c d e f"); }); await builder.RunAsync(args); """; - verifier.Execute(code, "", "root"); // root + verifier.Execute(code, "", "root"); verifier.Execute(code, "a", "a"); - verifier.Execute(code, "a b1", "a/b1"); - verifier.Execute(code, "a b2", "a/b2"); - verifier.Execute(code, "a b2 c", "a/b2/c"); - verifier.Execute(code, "a b2 d", "a/b2/d"); - verifier.Execute(code, "a b2 d e", "a/b2/d/e"); - verifier.Execute(code, "a b c d e f", "a/b/c/d/e/f"); + verifier.Execute(code, "a b1", "a b1"); + verifier.Execute(code, "a b2", "a b2"); + verifier.Execute(code, "a b2 c", "a b2 c"); + verifier.Execute(code, "a b2 d", "a b2 d"); + verifier.Execute(code, "a b2 d e", "a b2 d e"); + verifier.Execute(code, "a b c d e f", "a b c d e f"); } [Fact] @@ -103,23 +103,23 @@ public void WithargsAsync() builder.Add("", (int x, int y) => { Console.Write($"root {x} {y}"); }); builder.Add("a", (int x, int y) => { Console.Write($"a {x} {y}"); }); -builder.Add("a/b1", (int x, int y) => { Console.Write($"a/b1 {x} {y}"); }); -builder.Add("a/b2", (int x, int y) => { Console.Write($"a/b2 {x} {y}"); }); -builder.Add("a/b2/c", (int x, int y) => { Console.Write($"a/b2/c {x} {y}"); }); -builder.Add("a/b2/d", (int x, int y) => { Console.Write($"a/b2/d {x} {y}"); }); -builder.Add("a/b2/d/e", (int x, int y) => { Console.Write($"a/b2/d/e {x} {y}"); }); -builder.Add("a/b/c/d/e/f", (int x, int y) => { Console.Write($"a/b/c/d/e/f {x} {y}"); }); +builder.Add("a b1", (int x, int y) => { Console.Write($"a b1 {x} {y}"); }); +builder.Add("a b2", (int x, int y) => { Console.Write($"a b2 {x} {y}"); }); +builder.Add("a b2 c", (int x, int y) => { Console.Write($"a b2 c {x} {y}"); }); +builder.Add("a b2 d", (int x, int y) => { Console.Write($"a b2 d {x} {y}"); }); +builder.Add("a b2 d e", (int x, int y) => { Console.Write($"a b2 d e {x} {y}"); }); +builder.Add("a b c d e f", (int x, int y) => { Console.Write($"a b c d e f {x} {y}"); }); await builder.RunAsync(args); """; - verifier.Execute(code, "--x 10 --y 20", "root 10 20"); // root + verifier.Execute(code, "--x 10 --y 20", "root 10 20"); verifier.Execute(code, "a --x 10 --y 20", "a 10 20"); - verifier.Execute(code, "a b1 --x 10 --y 20", "a/b1 10 20"); - verifier.Execute(code, "a b2 --x 10 --y 20", "a/b2 10 20"); - verifier.Execute(code, "a b2 c --x 10 --y 20", "a/b2/c 10 20"); - verifier.Execute(code, "a b2 d --x 10 --y 20", "a/b2/d 10 20"); - verifier.Execute(code, "a b2 d e --x 10 --y 20", "a/b2/d/e 10 20"); - verifier.Execute(code, "a b c d e f --x 10 --y 20", "a/b/c/d/e/f 10 20"); + verifier.Execute(code, "a b1 --x 10 --y 20", "a b1 10 20"); + verifier.Execute(code, "a b2 --x 10 --y 20", "a b2 10 20"); + verifier.Execute(code, "a b2 c --x 10 --y 20", "a b2 c 10 20"); + verifier.Execute(code, "a b2 d --x 10 --y 20", "a b2 d 10 20"); + verifier.Execute(code, "a b2 d e --x 10 --y 20", "a b2 d e 10 20"); + verifier.Execute(code, "a b c d e f --x 10 --y 20", "a b c d e f 10 20"); } } From 40e31b516463625e31952b4c134b8c6648e78aff Mon Sep 17 00:00:00 2001 From: neuecc Date: Sun, 2 Jun 2024 00:54:47 +0900 Subject: [PATCH 48/54] support sync-asyncdisposable --- .../ConsoleAppGenerator.cs | 11 ++++++++++ src/ConsoleAppFramework/Emitter.cs | 14 ++++++++++--- .../ConsoleAppBuilderTest.cs | 21 +++++++++++++++++++ 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/ConsoleAppFramework/ConsoleAppGenerator.cs b/src/ConsoleAppFramework/ConsoleAppGenerator.cs index fed14f6..5db81de 100644 --- a/src/ConsoleAppFramework/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework/ConsoleAppGenerator.cs @@ -391,6 +391,17 @@ public void 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() diff --git a/src/ConsoleAppFramework/Emitter.cs b/src/ConsoleAppFramework/Emitter.cs index d665859..e663d57 100644 --- a/src/ConsoleAppFramework/Emitter.cs +++ b/src/ConsoleAppFramework/Emitter.cs @@ -236,12 +236,20 @@ public void EmitRun(SourceBuilder sb, CommandWithId commandWithId, bool isRunAsy // sync (false, true, true) => "using ", (false, true, false) => "using ", - (false, false, true) => "", // IAsyncDisposable but sync, can't call disposeasync...... + (false, false, true) => "__use_wrapper", // IAsyncDisposable but sync, needs special wrapper (false, false, false) => "" }; - sb.AppendLine($"{usingInstance}var instance = {command.CommandMethodInfo.BuildNew()};"); - invokeCommand = $"instance.{command.CommandMethodInfo.MethodName}({methodArguments})"; + if (usingInstance != "__use_wrapper") + { + sb.AppendLine($"{usingInstance}var instance = {command.CommandMethodInfo.BuildNew()};"); + invokeCommand = $"instance.{command.CommandMethodInfo.MethodName}({methodArguments})"; + } + else + { + sb.AppendLine($"using var instance = new SyncAsyncDisposeWrapper<{command.CommandMethodInfo.TypeFullName}>({command.CommandMethodInfo.BuildNew()});"); + invokeCommand = $"instance.Value.{command.CommandMethodInfo.MethodName}({methodArguments})"; + } } if (hasCancellationToken) diff --git a/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppBuilderTest.cs b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppBuilderTest.cs index 21c0c83..a6a9bf5 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppBuilderTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppBuilderTest.cs @@ -144,6 +144,27 @@ public void Dispose() builder.Add(); await builder.RunAsync(args); +public class MyClass : IAsyncDisposable +{ + public void Do() + { + Console.Write("yeah:"); + } + + public ValueTask DisposeAsync() + { + Console.Write("disposed!"); + return default; + } +} +""", "do", "yeah:disposed!"); + + // DisposeAsync: sync pattern + verifier.Execute(""" +var builder = ConsoleApp.Create(); +builder.Add(); +builder.Run(args); + public class MyClass : IAsyncDisposable { public void Do() From 2699a1526e2850d2287febd4ed16ee5100e291af Mon Sep 17 00:00:00 2001 From: neuecc Date: Sun, 2 Jun 2024 01:58:02 +0900 Subject: [PATCH 49/54] generate help for intellisense --- sandbox/GeneratorSandbox/Filters.cs | 19 ++++--- sandbox/GeneratorSandbox/Program.cs | 53 +++++++++++++++++-- .../ConsoleAppGenerator.cs | 20 +++++-- src/ConsoleAppFramework/Emitter.cs | 14 ++++- src/ConsoleAppFramework/Parser.cs | 2 +- 5 files changed, 91 insertions(+), 17 deletions(-) diff --git a/sandbox/GeneratorSandbox/Filters.cs b/sandbox/GeneratorSandbox/Filters.cs index a5c6b8c..d3f280d 100644 --- a/sandbox/GeneratorSandbox/Filters.cs +++ b/sandbox/GeneratorSandbox/Filters.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Reflection; using System.Text; using System.Threading.Tasks; @@ -59,17 +60,21 @@ public override async Task InvokeAsync(ConsoleAppContext context, CancellationTo } } -internal class MutexFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) +internal class PreventMultipleInstanceFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) { public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) { - // var name = context.MethodInfo.DeclaringType.Name + "." + context.MethodInfo.Name; - // using (var mutex = new Mutex(true, name, out var createdNew)) ; + // allow another command + // prevent: location + command + var basePath = Assembly.GetEntryAssembly()?.Location.Replace(Path.DirectorySeparatorChar, '_'); - //if (!createdNew) - //{ - // throw new Exception($"already running {name} in another process."); - //} + var mutexKey = $"{basePath}$$${context.CommandName}"; + + using var mutex = new Mutex(true, mutexKey, out var createdNew); + if (!createdNew) + { + throw new Exception($"already running command:{context.CommandName} in another process."); + } await Next.InvokeAsync(context, cancellationToken); } diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index 2360981..1297193 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -1,4 +1,5 @@ using ConsoleAppFramework; +using GeneratorSandbox; using System.ComponentModel.DataAnnotations; var serviceCollection = new MiniDI(); @@ -6,17 +7,61 @@ serviceCollection.Register(typeof(int), 9999); ConsoleApp.ServiceProvider = serviceCollection; -var builder = ConsoleApp.Create(); -builder.UseFilter(); -builder.Add("", (ConsoleAppContext ctx) => Console.Write("do")); +// ConsoleApp.Run(args, (int x, int y) => { }); +//// +//args = ["foo-bar-baz"]; -builder.Run(args); +////args = ["foo-bar-baz", "-h"]; +//var builder = ConsoleApp.Create(); +////builder.UseFilter(); + + +//builder.Add(); + +//builder. + + +public class MyCommand +{ + + /// + /// 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 void Dispose() + { + throw new NotImplementedException(); + } + + public void FooBarBaz(int hogeMogeHugahuga) + { + Console.WriteLine(hogeMogeHugahuga); + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + + public override bool Equals(object? obj) + { + return base.Equals(obj); + } + + public override string? ToString() + { + return base.ToString(); + } +} + internal class DIFilter(string foo, int bar, ConsoleAppFilter next) : ConsoleAppFilter(next) { diff --git a/src/ConsoleAppFramework/ConsoleAppGenerator.cs b/src/ConsoleAppFramework/ConsoleAppGenerator.cs index 5db81de..3c12336 100644 --- a/src/ConsoleAppFramework/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework/ConsoleAppGenerator.cs @@ -150,10 +150,22 @@ public static Action LogError 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; @@ -264,11 +276,11 @@ static void ValidateParameter(object? value, ParameterInfo parameter, Validation } [MethodImpl(MethodImplOptions.AggressiveInlining)] - static bool TryShowHelpOrVersion(ReadOnlySpan args, int parameterCount, int helpId) + static bool TryShowHelpOrVersion(ReadOnlySpan args, int requiredParameterCount, int helpId) { if (args.Length == 0) { - if (parameterCount == 0) return false; + if (requiredParameterCount == 0) return false; ShowHelp(helpId); return true; @@ -446,11 +458,11 @@ public Task RunAsync(string[] args) static partial void ShowHelp(int helpId); [MethodImpl(MethodImplOptions.AggressiveInlining)] - static bool TryShowHelpOrVersion(ReadOnlySpan args, int parameterCount, int helpId) + static bool TryShowHelpOrVersion(ReadOnlySpan args, int requiredParameterCount, int helpId) { if (args.Length == 0) { - if (parameterCount == 0) return false; + if (requiredParameterCount == 0) return false; ShowHelp(helpId); return true; diff --git a/src/ConsoleAppFramework/Emitter.cs b/src/ConsoleAppFramework/Emitter.cs index e663d57..0174458 100644 --- a/src/ConsoleAppFramework/Emitter.cs +++ b/src/ConsoleAppFramework/Emitter.cs @@ -15,6 +15,7 @@ public void EmitRun(SourceBuilder sb, CommandWithId commandWithId, bool isRunAsy var hasArgument = command.Parameters.Any(x => x.IsArgument); var hasValidation = command.Parameters.Any(x => x.HasValidation); var parsableParameterCount = command.Parameters.Count(x => x.IsParsable); + var requiredParsableParameterCount = command.Parameters.Count(x => x.IsParsable && x.RequireCheckArgumentParsed); if (command.HasFilter) { @@ -44,10 +45,21 @@ public void EmitRun(SourceBuilder sb, CommandWithId commandWithId, bool isRunAsy var filterCancellationToken = command.HasFilter ? ", ConsoleAppContext context, CancellationToken cancellationToken" : ""; var rawArgs = !emitForBuilder ? "" : "string[] rawArgs, "; + if (!emitForBuilder) + { + sb.AppendLine("/// "); + var help = CommandHelpBuilder.BuildCommandHelpMessage(commandWithId.Command); + foreach (var line in help.Split([Environment.NewLine], StringSplitOptions.None)) + { + sb.AppendLine($"/// {line.Replace("<", "<").Replace(">", ">")}
"); + } + sb.AppendLine("///
"); + } + // method signature using (sb.BeginBlock($"{accessibility} static {unsafeCode}{returnType} {methodName}({rawArgs}{argsType} args{commandMethodType}{filterCancellationToken})")) { - sb.AppendLine($"if (TryShowHelpOrVersion(args, {parsableParameterCount}, {commandWithId.Id})) return;"); + sb.AppendLine($"if (TryShowHelpOrVersion(args, {requiredParsableParameterCount}, {commandWithId.Id})) return;"); sb.AppendLine(); // prepare argument variables diff --git a/src/ConsoleAppFramework/Parser.cs b/src/ConsoleAppFramework/Parser.cs index 7a58205..746c565 100644 --- a/src/ConsoleAppFramework/Parser.cs +++ b/src/ConsoleAppFramework/Parser.cs @@ -84,7 +84,7 @@ internal class Parser(SourceProductionContext context, InvocationExpressionSynta .OfType() .Where(x => x.DeclaredAccessibility == Accessibility.Public && !x.IsStatic) .Where(x => x.MethodKind == Microsoft.CodeAnalysis.MethodKind.Ordinary) - .Where(x => !(x.Name is "Dispose" or "DisposeAsync")) + .Where(x => !(x.Name is "Dispose" or "DisposeAsync" or "GetHashCode" or "Equals" or "ToString")) .ToArray(); var publicConstructors = type.GetMembers() From 9796074740f97339d19adee573d508cc264b38b7 Mon Sep 17 00:00:00 2001 From: neuecc Date: Sun, 2 Jun 2024 02:24:40 +0900 Subject: [PATCH 50/54] extra impl is done --- sandbox/GeneratorSandbox/Filters.cs | 7 +-- .../ConsoleAppContextTest.cs | 50 +++++++++++++++++++ 2 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 tests/ConsoleAppFramework.GeneratorTests/ConsoleAppContextTest.cs diff --git a/sandbox/GeneratorSandbox/Filters.cs b/sandbox/GeneratorSandbox/Filters.cs index d3f280d..70418bf 100644 --- a/sandbox/GeneratorSandbox/Filters.cs +++ b/sandbox/GeneratorSandbox/Filters.cs @@ -60,15 +60,12 @@ public override async Task InvokeAsync(ConsoleAppContext context, CancellationTo } } -internal class PreventMultipleInstanceFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) +internal class PreventMultipleSameCommandInvokeFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) { public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) { - // allow another command - // prevent: location + command var basePath = Assembly.GetEntryAssembly()?.Location.Replace(Path.DirectorySeparatorChar, '_'); - - var mutexKey = $"{basePath}$$${context.CommandName}"; + var mutexKey = $"{basePath}$$${context.CommandName}"; // lock per command-name using var mutex = new Mutex(true, mutexKey, out var createdNew); if (!createdNew) diff --git a/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppContextTest.cs b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppContextTest.cs new file mode 100644 index 0000000..3764517 --- /dev/null +++ b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppContextTest.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit.Abstractions; + +namespace ConsoleAppFramework.GeneratorTests; + +public class ConsoleAppContextTest(ITestOutputHelper output) +{ + VerifyHelper verifier = new VerifyHelper(output, "CAF"); + + [Fact] + public void ForLambda() + { + verifier.Execute(""" +ConsoleApp.Run(args, (ConsoleAppContext ctx) => { Console.Write(ctx.Arguments.Length); }); +""", args: "", expected: "0"); + } + + [Fact] + public void ForMethod() + { + verifier.Execute(""" +var builder = ConsoleApp.Create(); + +builder.UseFilter(); + +builder.Add("", Hello); + +builder.Run(args); + +void Hello(ConsoleAppContext ctx) +{ + Console.Write(ctx.State); +} + +internal class StateFilter(ConsoleAppFilter next) + : ConsoleAppFilter(next) +{ + public override Task InvokeAsync(ConsoleAppContext context,CancellationToken cancellationToken) + { + Console.Write(1); + return Next.InvokeAsync(context with { State = 2 }, cancellationToken); + } +} +""", args: "", expected: "12"); + } +} From 24e39a1acafcf24b0bfc5ceb63bd6a0ed055f10b Mon Sep 17 00:00:00 2001 From: neuecc Date: Sun, 2 Jun 2024 02:31:05 +0900 Subject: [PATCH 51/54] a --- ReadMe.md | 11 ++--- .../ConsoleAppContextTest.cs | 9 +--- .../DITest.cs | 41 +++++++++++++++++++ .../GlobalUsings.cs | 1 + 4 files changed, 49 insertions(+), 13 deletions(-) create mode 100644 tests/ConsoleAppFramework.GeneratorTests/DITest.cs diff --git a/ReadMe.md b/ReadMe.md index 92284aa..6ba4746 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -299,15 +299,15 @@ If the class implements `IDisposable` or `IAsyncDisposable`, the Dispose or Disp ### Nested command -You can create a deep command hierarchy by adding commands with paths separated by `/` when registering them. This allows you to add commands at nested levels. +You can create a deep command hierarchy by adding commands with paths separated by ` ` when registering them. This allows you to add commands at nested levels. ```csharp var app = ConsoleApp.Create(); app.Add("foo", () => { }); -app.Add("foo/bar", () => { }); -app.Add("foo/bar/barbaz", () => { }); -app.Add("foo/baz", () => { }); +app.Add("foo bar", () => { }); +app.Add("foo bar barbaz", () => { }); +app.Add("foo baz", () => { }); // Commands: // foo @@ -417,7 +417,8 @@ Attribute based parameters validation --- - +ConsoleAppContext +--- diff --git a/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppContextTest.cs b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppContextTest.cs index 3764517..674270c 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppContextTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppContextTest.cs @@ -1,11 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Xunit.Abstractions; - -namespace ConsoleAppFramework.GeneratorTests; +namespace ConsoleAppFramework.GeneratorTests; public class ConsoleAppContextTest(ITestOutputHelper output) { diff --git a/tests/ConsoleAppFramework.GeneratorTests/DITest.cs b/tests/ConsoleAppFramework.GeneratorTests/DITest.cs new file mode 100644 index 0000000..76ab453 --- /dev/null +++ b/tests/ConsoleAppFramework.GeneratorTests/DITest.cs @@ -0,0 +1,41 @@ +namespace ConsoleAppFramework.GeneratorTests; + +public class DITest(ITestOutputHelper output) +{ + VerifyHelper verifier = new VerifyHelper(output, "CAF"); + + [Fact] + public void ServiceProvider() + { + verifier.Execute(""" +#nullable enable + +var di = new MiniDI(); +di.Register(typeof(MyClass), new MyClass("foo")); +ConsoleApp.ServiceProvider = di; + +ConsoleApp.Run(args, ([FromServices] MyClass mc, int x, int y) => { Console.Write(mc.Name + ":" + x + ":" + y); }); + + +class MiniDI : IServiceProvider +{ + System.Collections.Generic.Dictionary dict = new(); + + public void Register(Type type, object instance) + { + dict[type] = instance; + } + + public object? GetService(Type serviceType) + { + return dict.TryGetValue(serviceType, out var instance) ? instance : null; + } +} + +class MyClass(string name) +{ + public string Name => name; +} +""", args: "--x 10 --y 20", expected: "foo:10:20"); + } +} \ No newline at end of file diff --git a/tests/ConsoleAppFramework.GeneratorTests/GlobalUsings.cs b/tests/ConsoleAppFramework.GeneratorTests/GlobalUsings.cs index 3b07b9b..9cb7279 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/GlobalUsings.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/GlobalUsings.cs @@ -1,4 +1,5 @@ global using Xunit; +global using Xunit.Abstractions; global using FluentAssertions; // CSharpGeneratorRunner.CompileAndExecute uses stdout hook(replace Console.Out) From e279ae66bcc7a22b4a5d2076117788cf21c0a88b Mon Sep 17 00:00:00 2001 From: neuecc Date: Sun, 2 Jun 2024 22:25:34 +0900 Subject: [PATCH 52/54] Readmeing --- ReadMe.md | 362 ++++++++++++++++-- .../CliFrameworkBenchmark.csproj | 4 + sandbox/GeneratorSandbox/Filters.cs | 62 ++- .../GeneratorSandbox/GeneratorSandbox.csproj | 8 + sandbox/GeneratorSandbox/Program.cs | 70 ++-- sandbox/GeneratorSandbox/appsettings.json | 8 + 6 files changed, 431 insertions(+), 83 deletions(-) create mode 100644 sandbox/GeneratorSandbox/appsettings.json diff --git a/ReadMe.md b/ReadMe.md index 6ba4746..ceff529 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -118,7 +118,7 @@ ConsoleAppFramework offers a rich set of features as a framework. The Source Gen * Setting option aliases and descriptions from code document comment * `System.ComponentModel.DataAnnotations` attribute-based Validation * Dependency Injection for command registration by type and public methods -* Microsoft.Extensions(Logging, Configuration, etc...) integration +* `Microsoft.Extensions`(Logging, Configuration, etc...) integration * High performance value parsing via `ISpanParsable` * Parsing of params arrays * Parsing of JSON arguments @@ -148,7 +148,7 @@ You can execute command like `sampletool --name "foo"`. * The return value can be `void`, `int`, `Task`, or `Task` * If an `int` is returned, that value will be set to `Environment.ExitCode` * By default, option argument names are converted to `--lower-kebab-case` - * For example, `XmlReader` becomes `xml-reader` + * For example, `jsonValue` becomes `--json-value` * Option argument names are case-insensitive, but lower-case matches faster When passing a method, you can write it as follows: @@ -299,7 +299,7 @@ If the class implements `IDisposable` or `IAsyncDisposable`, the Dispose or Disp ### Nested command -You can create a deep command hierarchy by adding commands with paths separated by ` ` when registering them. This allows you to add commands at nested levels. +You can create a deep command hierarchy by adding commands with paths separated by space(` `) when registering them. This allows you to add commands at nested levels. ```csharp var app = ConsoleApp.Create(); @@ -330,6 +330,10 @@ app.Add("foo"); app.Run(args); ``` +### Performance of Commands + +TODO:NANIKA KAKU + Parse and Value Binding --- @@ -392,6 +396,10 @@ public class Vector3ParserAttribute : Attribute, IArgumentParser } ``` + + + + CancellationToken(Gracefully Shutdown) and Timeout --- @@ -409,82 +417,372 @@ If the method returns `int` or `Task` or `ValueTask value, ConsoleAppF -Log ---- - Attribute based parameters validation --- -ConsoleAppContext ---- -Filter(Middleware) Pipline +Filter(Middleware) Pipline / ConsoleAppContext --- +Filters are provided as a mechanism to hook into the execution before and after. To use filters, define an `internal class` that implements `ConsoleAppFilter`. + +```csharp +internal class NopFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) // ctor needs `ConsoleAppFilter next` and call base(next) +{ + // implement InvokeAsync as filter body + public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) + { + try + { + /* on before */ + await Next.InvokeAsync(context, cancellationToken); // invoke next filter or command body + /* on after */ + } + catch + { + /* on error */ + throw; + } + finally + { + /* on finally */ + } + } +} +``` + +Filters can be attached multiple times to "global", "class", or "method" using `UseFilter` or `[ConsoleAppFilter]`. The order of filters is global → class → method, and the execution order is determined by the definition order from top to bottom. + +```csharp +var app = ConsoleApp.Create(); + +// global filters +app.UseFilter(); //order 1 +app.UseFilter(); //order 2 + +app.Add(); +app.Run(args); + +// per class filters +[ConsoleAppFilter] // order 3 +[ConsoleAppFilter] // order 4 +public class MyCommand +{ + // per method filters + [ConsoleAppFilter] // order 5 + [ConsoleAppFilter] // order 6 + public void Echo(string msg) => Console.WriteLine(msg); +} +``` -// TODO:samples? change exit code, log, etc... -// TODO: how to share filter +Filters allow various processes to be shared. For example, the process of measuring execution time can be written as follows: ```csharp -internal class TimestampFilter(ConsoleAppFilter next) - : ConsoleAppFilter(next) +internal class LogRunningTimeFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) { - public override Task InvokeAsync(CancellationToken cancellationToken) + public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) { - Console.WriteLine("filter1"); - return Next.InvokeAsync(cancellationToken); + var startTime = Stopwatch.GetTimestamp(); + ConsoleApp.Log($"Execute command at {DateTime.UtcNow.ToLocalTime()}"); // LocalTime for human readable time + try + { + await Next.InvokeAsync(context, cancellationToken); + ConsoleApp.Log($"Command execute successfully at {DateTime.UtcNow.ToLocalTime()}, Elapsed: " + (Stopwatch.GetElapsedTime(startTime))); + } + catch + { + ConsoleApp.Log($"Command execute failed at {DateTime.UtcNow.ToLocalTime()}, Elapsed: " + (Stopwatch.GetElapsedTime(startTime))); + throw; + } } } +``` +In case of an exception, the `ExitCode` is usually `1`, and the stack trace is also displayed. However, by applying an exception handling filter, the behavior can be changed. -internal class LogExecutionTimeFilter(ConsoleAppFilter next) - : ConsoleAppFilter(next) +```csharp +internal class ChangeExitCodeFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) { - public override async Task InvokeAsync(CancellationToken cancellationToken) + public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) { - var startingTime = Stopwatch.GetTimestamp(); try { - await Next.InvokeAsync(cancellationToken); + await Next.InvokeAsync(context, cancellationToken); } - finally + catch (Exception ex) + { + if (ex is OperationCanceledException) return; + + Environment.ExitCode = 9999; // change custom exit code + ConsoleApp.LogError(ex.Message); // .ToString() shows stacktrace, .Message can avoid showing stacktrace to user. + } + } +} +``` + +Filters are executed after the command name routing is completed. If you want to prohibit multiple executions for each command name, you can use `ConsoleAppContext.CommandName` as the key. + +```csharp +internal class PreventMultipleSameCommandInvokeFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) +{ + public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) + { + var basePath = Assembly.GetEntryAssembly()?.Location.Replace(Path.DirectorySeparatorChar, '_'); + var mutexKey = $"{basePath}$$${context.CommandName}"; // lock per command-name + + using var mutex = new Mutex(true, mutexKey, out var createdNew); + if (!createdNew) { - var elapsed = Stopwatch.GetElapsedTime(startingTime); - ConsoleApp.Log($"Execution Time: {elapsed}"); + throw new Exception($"already running command:{context.CommandName} in another process."); } + + await Next.InvokeAsync(context, cancellationToken); } } ``` +If you want to pass values between filters or to commands, you can use `ConsoleAppContext.State`. For example, if you want to perform authentication processing and pass around the ID, you can write code like the following. Since `ConsoleAppContext` is an immutable record, you need to pass the rewritten context to Next using the `with` syntax. + +```csharp +internal class AuthenticationFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) +{ + public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) + { + var requestId = Guid.NewGuid(); + var userId = await GetUserIdAsync(); + + // setup new state to context + var authedContext = context with { State = new ApplicationContext(requestId, userId) }; + await Next.InvokeAsync(authedContext, cancellationToken); + } + + // get user-id from DB/auth saas/others + async Task GetUserIdAsync() + { + await Task.Delay(TimeSpan.FromSeconds(1)); + return 1999; + } +} + +record class ApplicationContext(Guid RequiestId, int UserId); +``` + +Commands can accept `ConsoleAppContext` as an argument. This allows using the values processed by filters. + +```csharp +var app = ConsoleApp.Create(); + +app.UseFilter(); + +app.Add("", (int x, int y, ConsoleAppContext context) => +{ + var appContext = (ApplicationContext)context.State!; + var requestId = appContext.RequiestId; + var userId = appContext.UserId; + + Console.WriteLine($"Request:{requestId} User:{userId} Sum:{x + y}"); +}); + +app.Run(args); +``` + +`ConsoleAppContext` also has a `ConsoleAppContext.Arguments` property that allows you to obtain the (`string[] args`) passed to Run/RunAsync. + +### Sharing Filters Between Projects + +`ConsoleAppFilter` is defined as `internal` for each project by the Source Generator, so the filters to be implemented must also be `internal`. Sharing at the csproj or DLL level is not possible, so source code needs to be shared by linking references. + +```xml + + + +``` + +If you want to share via NuGet, you need to distribute the source code or distribute it in a format that includes the source code using `.props`. + +### Performance of filter +In general frameworks, filters are dynamically added at runtime, resulting in a variable number of filters. Therefore, they need to be allocated using a dynamic array. In ConsoleAppFramework, the number of filters is statically determined at compile time, eliminating the need for any additional allocations such as arrays or lambda expression captures. The allocation amount is equal to the number of filter classes being used plus 1 (for wrapping the command method), resulting in the shortest execution path. + +```csharp +app.UseFilter(); +app.UseFilter(); +app.UseFilter(); +app.UseFilter(); +app.UseFilter(); + +// The above code will generate the following code: + +sealed class Command0Invoker(string[] args, Action command) : ConsoleAppFilter(null!) +{ + public ConsoleAppFilter BuildFilter() + { + var filter0 = new NopFilter(this); + var filter1 = new NopFilter(filter0); + var filter2 = new NopFilter(filter1); + var filter3 = new NopFilter(filter2); + var filter4 = new NopFilter(filter3); + return filter4; + } + + public override Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) + { + return RunCommand0Async(context.Arguments, args, command, context, cancellationToken); + } +} +``` +When an `async Task` completes synchronously, it returns the equivalent of `Task.CompletedTask`, so `ValueTask` is not necessary. Dependency Injection(Logging, Configuration, etc...) --- +The execution processing of `ConsoleAppFramework` fully supports `DI`. When you want to use a logger, read a configuration, or share processing with an ASP.NET project, using `Microsoft.Extensions.DependencyInjection` or other DI libraries can make processing convenient. + +Lambda expressions passed to Run, class constructors, methods, and filter constructors can inject services obtained from `IServiceProvider`. Let's look at a minimal example. Setting any `System.IServiceProvider` to `ConsoleApp.ServiceProvider` enables DI throughout the system. + +```csharp +// Microsoft.Extensions.DependencyInjection +var services = new ServiceCollection(); +services.AddTransient(); + +using var serviceProvider = services.BuildServiceProvider(); + +// Any DI library can be used as long as it can create an IServiceProvider +ConsoleApp.ServiceProvider = serviceProvider; + +// When passing to a lambda expression/method, using [FromServices] indicates that it is passed via DI, not as a parameter +ConsoleApp.Run(args, ([FromServices]MyService service, int x, int y) => Console.WriteLine(x + y)); +``` + +When passing to a lambda expression or method, the `[FromServices]` attribute is used to distinguish it from command parameters. When passing a class, Constructor Injection can be used, resulting in a simpler appearance. -`[FromServices]` +Let's try injecting a logger and enabling output to a file. The libraries used are Microsoft.Extensions.Logging and [Cysharp/ZLogger](https://github.com/Cysharp/ZLogger/) (a high-performance logger built on top of MS.E.Logging). -// TODO: minimum single context ```csharp -public class FilterContext : IServiceProvider +// Package Import: ZLogger +var services = new ServiceCollection(); +services.AddLogging(x => { - public long Timestamp { get; set; } - public Guid UserId { get; set; } + x.ClearProviders(); + x.SetMinimumLevel(LogLevel.Trace); + x.AddZLoggerConsole(); + x.AddZLoggerFile("log.txt"); +}); + +using var serviceProvider = services.BuildServiceProvider(); // using for logger flush(important!) +ConsoleApp.ServiceProvider = serviceProvider; + +var app = ConsoleApp.Create(); +app.Add(); +app.Run(args); - object IServiceProvider.GetService(Type serviceType) +// inject logger to constructor +public class MyCommand(ILogger logger) +{ + [Command("")] + public void Echo(string msg) { - if (serviceType == typeof(FilterContext)) return this; - throw new InvalidOperationException("Type is invalid:" + serviceType); + logger.ZLogInformation($"Message is {msg}"); } } ``` +`ConsoleApp` has replaceable default logging methods `ConsoleApp.Log` and `ConsoleApp.LogError` used for Help display and exception handling. If using `ILogger`, it's better to replace these as well. +```csharp +using var serviceProvider = services.BuildServiceProvider(); // using for cleanup(important) +ConsoleApp.ServiceProvider = serviceProvider; + +// setup ConsoleApp system logger +var logger = serviceProvider.GetRequiredService>(); +ConsoleApp.Log = msg => logger.LogInformation(msg); +ConsoleApp.LogError = msg => logger.LogError(msg); +``` +DI can also be effectively used when reading application configuration from `appsettings.json`. For example, suppose you have the following JSON file. + +```json +{ + "Position": { + "Title": "Editor", + "Name": "Joe Smith" + }, + "MyKey": "My appsettings.json Value", + "AllowedHosts": "*" +} +``` + +Using `Microsoft.Extensions.Configuration.Json`, reading, binding, and registering with DI can be done as follows. + +```csharp +// Package Import: Microsoft.Extensions.Configuration.Json +var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json") + .Build(); + +// Bind to services +var services = new ServiceCollection(); +services.Configure(configuration.GetSection("Position")); + +using var serviceProvider = services.BuildServiceProvider(); +ConsoleApp.ServiceProvider = serviceProvider; + +var app = ConsoleApp.Create(); +app.Add(); +app.Run(args); + +// inject options +public class MyCommand(IOptions options) +{ + [Command("")] + public void Echo(string msg) + { + ConsoleApp.Log($"Binded Option: {options.Value.Title} {options.Value.Name}"); + } +} + +public class PositionOptions +{ + public string Title { get; set; } = ""; + public string Name { get; set; } = ""; +} +``` + +If you have other applications such as ASP.NET in the entire project and want to use common DI and configuration set up using `Microsoft.Extensions.Hosting`, you can share them by setting the `IServiceProvider` of `IHost` after building. + +```csharp +// Package Import: Microsoft.Extensions.Hosting +var builder = Host.CreateApplicationBuilder(); // don't pass args. + +using var host = builder.Build(); // using +ConsoleApp.ServiceProvider = host.Services; // use host ServiceProvider + +ConsoleApp.Run(args, ([FromServices] ILogger logger) => logger.LogInformation("Hello World!")); +``` + +ConsoleAppFramework has its own lifetime management (see the [CancellationToken(Gracefully Shutdown) and Timeout](#cancellationtokengracefully-shutdown-and-timeout) section), so Host's Start/Stop is not necessary. However, be sure to use the Host itself. + +As it is, the DI scope is not set, but by using a global filter, you can add a scope for each command execution. `ConsoleAppFilter` can also inject services via constructor injection, so let's get the `IServiceProvider`. + +```csharp +var app = ConsoleApp.Create(); +app.UseFilter(); + +internal class ServiceProviderScopeFilter(IServiceProvider serviceProvider, ConsoleAppFilter next) : ConsoleAppFilter(next) +{ + public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) + { + // create Microsoft.Extensions.DependencyInjection scope + await using var scope = serviceProvider.CreateAsyncScope(); + await Next.InvokeAsync(context, cancellationToken); + } +} +``` Publish to executable file --- diff --git a/sandbox/CliFrameworkBenchmark/CliFrameworkBenchmark.csproj b/sandbox/CliFrameworkBenchmark/CliFrameworkBenchmark.csproj index e8e750e..5b0c95f 100644 --- a/sandbox/CliFrameworkBenchmark/CliFrameworkBenchmark.csproj +++ b/sandbox/CliFrameworkBenchmark/CliFrameworkBenchmark.csproj @@ -9,6 +9,10 @@ false + + + + diff --git a/sandbox/GeneratorSandbox/Filters.cs b/sandbox/GeneratorSandbox/Filters.cs index 70418bf..d3d0b07 100644 --- a/sandbox/GeneratorSandbox/Filters.cs +++ b/sandbox/GeneratorSandbox/Filters.cs @@ -1,26 +1,56 @@ -using ConsoleAppFramework; -using System; -using System.Collections.Generic; + +using ConsoleAppFramework; +using Microsoft.Extensions.DependencyInjection; using System.Diagnostics; -using System.Linq; using System.Reflection; -using System.Text; -using System.Threading.Tasks; namespace GeneratorSandbox; +// ReadMe sample filters +internal class NopFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) +{ + public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) + { + try + { + /* on before */ + await Next.InvokeAsync(context, cancellationToken); // next + /* on after */ + } + catch + { + /* on error */ + throw; + } + finally + { + /* on finally */ + } + } +} +internal class AuthenticationFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) +{ + public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) + { + var requestId = Guid.NewGuid(); + var userId = await GetUserIdAsync(); + // setup new state to context + var authedContext = context with { State = new ApplicationContext(requestId, userId) }; + await Next.InvokeAsync(authedContext, cancellationToken); + } -internal class NopFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) -{ - public override Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) + // get user-id from DB/auth saas/others + async Task GetUserIdAsync() { - return Next.InvokeAsync(context, cancellationToken); + await Task.Delay(TimeSpan.FromSeconds(1)); + return 1999; } } +record class ApplicationContext(Guid RequiestId, int UserId); internal class LogRunningTimeFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) { @@ -41,7 +71,6 @@ public override async Task InvokeAsync(ConsoleAppContext context, CancellationTo } } - internal class ChangeExitCodeFilter(ConsoleAppFilter next) : ConsoleAppFilter(next) { public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) @@ -73,6 +102,17 @@ public override async Task InvokeAsync(ConsoleAppContext context, CancellationTo throw new Exception($"already running command:{context.CommandName} in another process."); } + await Next.InvokeAsync(context, cancellationToken); + } +} + + +internal class ServiceProviderScopeFilter(IServiceProvider serviceProvider, ConsoleAppFilter next) : ConsoleAppFilter(next) +{ + public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) + { + // create Microsoft.Extensions.DependencyInjection scope + await using var scope = serviceProvider.CreateAsyncScope(); await Next.InvokeAsync(context, cancellationToken); } } \ No newline at end of file diff --git a/sandbox/GeneratorSandbox/GeneratorSandbox.csproj b/sandbox/GeneratorSandbox/GeneratorSandbox.csproj index d4d4a9f..8383bef 100644 --- a/sandbox/GeneratorSandbox/GeneratorSandbox.csproj +++ b/sandbox/GeneratorSandbox/GeneratorSandbox.csproj @@ -11,8 +11,10 @@ + + @@ -22,4 +24,10 @@ + + + PreserveNewest + + + diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index 1297193..1f33eb0 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -1,66 +1,56 @@ using ConsoleAppFramework; using GeneratorSandbox; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using System.ComponentModel.DataAnnotations; +using System.Threading.Channels; +using ZLogger; -var serviceCollection = new MiniDI(); -serviceCollection.Register(typeof(string), "hoge!"); -serviceCollection.Register(typeof(int), 9999); -ConsoleApp.ServiceProvider = serviceCollection; +// args = ["--msg", "foobarbaz"]; +// Microsoft.Extensions.DependencyInjection -// ConsoleApp.Run(args, (int x, int y) => { }); -//// -//args = ["foo-bar-baz"]; +// Package Import: Microsoft.Extensions.Hosting +var builder = Host.CreateApplicationBuilder(); // don't pass args. -////args = ["foo-bar-baz", "-h"]; +using var host = builder.Build(); // using +ConsoleApp.ServiceProvider = host.Services; // use host ServiceProvider -//var builder = ConsoleApp.Create(); +ConsoleApp.Run(args, ([FromServices] ILogger logger) => logger.LogInformation("Hello World!")); -////builder.UseFilter(); -//builder.Add(); +// inject logger +public class MyCommand(ILogger logger, IOptions options) +{ + [Command("")] + public void Echo(string msg) + { + logger.ZLogTrace($"Binded Option: {options.Value.Title} {options.Value.Name}"); + logger.ZLogInformation($"Message is {msg}"); + } +} -//builder. -public class MyCommand +public class PositionOptions { + public string Title { get; set; } = ""; + public string Name { get; set; } = ""; +} + + - /// - /// 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 void Dispose() - { - throw new NotImplementedException(); - } - public void FooBarBaz(int hogeMogeHugahuga) - { - Console.WriteLine(hogeMogeHugahuga); - } - public override int GetHashCode() - { - return base.GetHashCode(); - } - public override bool Equals(object? obj) - { - return base.Equals(obj); - } - public override string? ToString() - { - return base.ToString(); - } -} internal class DIFilter(string foo, int bar, ConsoleAppFilter next) : ConsoleAppFilter(next) diff --git a/sandbox/GeneratorSandbox/appsettings.json b/sandbox/GeneratorSandbox/appsettings.json new file mode 100644 index 0000000..442ff6b --- /dev/null +++ b/sandbox/GeneratorSandbox/appsettings.json @@ -0,0 +1,8 @@ +{ + "Position": { + "Title": "Editor", + "Name": "Joe Smith" + }, + "MyKey": "My appsettings.json Value", + "AllowedHosts": "*" +} \ No newline at end of file From a15bb6d0a5d86d28c8613d545e7795fdf7a525ec Mon Sep 17 00:00:00 2001 From: neuecc Date: Mon, 3 Jun 2024 03:06:12 +0900 Subject: [PATCH 53/54] done ReadMe --- ConsoleAppFramework.sln | 7 + ReadMe.md | 204 ++++++++++++++++++++++++---- sandbox/GeneratorSandbox/Program.cs | 61 ++++++++- sandbox/NativeAot/NativeAot.csproj | 20 +++ sandbox/NativeAot/Program.cs | 3 + 5 files changed, 265 insertions(+), 30 deletions(-) create mode 100644 sandbox/NativeAot/NativeAot.csproj create mode 100644 sandbox/NativeAot/Program.cs diff --git a/ConsoleAppFramework.sln b/ConsoleAppFramework.sln index 9ae6042..a591f4e 100644 --- a/ConsoleAppFramework.sln +++ b/ConsoleAppFramework.sln @@ -26,6 +26,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFrameworkBenchmark", "sa EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleAppFramework.GeneratorTests", "tests\ConsoleAppFramework.GeneratorTests\ConsoleAppFramework.GeneratorTests.csproj", "{C54F7FE8-650A-4DC7-877F-0DE929351800}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NativeAot", "sandbox\NativeAot\NativeAot.csproj", "{EC1A3299-6597-4AD2-92DE-EDF309875A97}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -48,6 +50,10 @@ Global {C54F7FE8-650A-4DC7-877F-0DE929351800}.Debug|Any CPU.Build.0 = Debug|Any CPU {C54F7FE8-650A-4DC7-877F-0DE929351800}.Release|Any CPU.ActiveCfg = Release|Any CPU {C54F7FE8-650A-4DC7-877F-0DE929351800}.Release|Any CPU.Build.0 = Release|Any CPU + {EC1A3299-6597-4AD2-92DE-EDF309875A97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EC1A3299-6597-4AD2-92DE-EDF309875A97}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EC1A3299-6597-4AD2-92DE-EDF309875A97}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EC1A3299-6597-4AD2-92DE-EDF309875A97}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -57,6 +63,7 @@ Global {ACDA48BA-0BFE-4917-B335-7836DAA5929A} = {A2CF2984-E8E2-48FC-B5A1-58D74A2467E6} {F558E4F2-1AB0-4634-B613-69DFE79894AF} = {A2CF2984-E8E2-48FC-B5A1-58D74A2467E6} {C54F7FE8-650A-4DC7-877F-0DE929351800} = {AAD2D900-C305-4449-A9FC-6C7696FFEDFA} + {EC1A3299-6597-4AD2-92DE-EDF309875A97} = {A2CF2984-E8E2-48FC-B5A1-58D74A2467E6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7F3E353A-C125-4020-8481-11DC6496358C} diff --git a/ReadMe.md b/ReadMe.md index ceff529..3a3662f 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -332,40 +332,151 @@ app.Run(args); ### Performance of Commands -TODO:NANIKA KAKU +In `ConsoleAppFramework`, the number and types of registered commands are statically determined at compile time. For example, let's register the following four commands: + +```csharp +app.Add("foo", () => { }); +app.Add("foo bar", (int x, int y) => { }); +app.Add("foo bar barbaz", (DateTime dateTime) => { }); +app.Add("foo baz", async (string foo = "test", CancellationToken cancellationToken = default) => { }); +``` + +The Source Generator generates four fields and holds them with specific types. + +```csharp +partial struct ConsoleAppBuilder +{ + Action command0 = default!; + Action command1 = default!; + Action command2 = default!; + Func command3 = default!; + + partial void AddCore(string commandName, Delegate command) + { + switch (commandName) + { + case "foo": + this.command0 = Unsafe.As(command); + break; + case "foo bar": + this.command1 = Unsafe.As>(command); + break; + case "foo bar barbaz": + this.command2 = Unsafe.As>(command); + break; + case "foo baz": + this.command3 = Unsafe.As>(command); + break; + default: + break; + } + } +} +``` + +This ensures the fastest execution speed without any additional unnecessary allocations such as arrays and without any boxing since it holds static delegate types. + +Command routing also generates a switch of nested string constants. + +```csharp +partial void RunCore(string[] args) +{ + if (args.Length == 0) + { + ShowHelp(-1); + return; + } + switch (args[0]) + { + case "foo": + if (args.Length == 1) + { + RunCommand0(args, args.AsSpan(1), command0); + return; + } + switch (args[1]) + { + case "bar": + if (args.Length == 2) + { + RunCommand1(args, args.AsSpan(2), command1); + return; + } + switch (args[2]) + { + case "barbaz": + RunCommand2(args, args.AsSpan(3), command2); + break; + default: + RunCommand1(args, args.AsSpan(2), command1); + break; + } + break; + case "baz": + RunCommand3(args, args.AsSpan(2), command3); + break; + default: + RunCommand0(args, args.AsSpan(1), command0); + break; + } + break; + default: + ShowHelp(-1); + break; + } +} +``` + +The C# compiler performs complex generation for string constant switches, making them extremely fast, and it would be difficult to achieve faster routing than this. Parse and Value Binding --- +The method parameter names and types determine how to parse and bind values from the command-line arguments. When using lambda expressions, optional values and `params` arrays supported from C# 12 are also supported. +```csharp +ConsoleApp.Run(args, ( + [Argument]DateTime dateTime, // Argument + [Argument]Guid guidvalue, // + int intVar, // required + bool boolFlag, // flag + MyEnum enumValue, // enum + int[] array, // array + MyClass obj, // object + string optional = "abcde", // optional + double? nullableValue = null, // nullable + params string[] paramsArray // params + ) => { }); +``` +When using `ConsoleApp.Run`, you can check the syntax of the command line in the tooltip to see how it is generated. +![image](https://github.com/Cysharp/ConsoleAppFramework/assets/46207/af480566-adac-4767-bd5e-af89ab6d71f1) -// TODO:reason and policy of limitation of parsing - -`[Argument]` +For the rules on converting parameter names to option names, aliases, and how to set documentation, refer to the [Option aliases](#option-aliases-and-help-version) section. -`bool` +Parameters marked with the `[Argument]` attribute receive values in order without parameter names. This attribute can only be set on sequential parameters from the beginning. +To convert from string arguments to various types, basic primitive types (`string`, `char`, `sbyte`, `byte`, `short`, `int`, `long`, `uint`, `ushort`, `ulong`, `decimal`, `float`, `double`) use `TryParse`. For types that implement `ISpanParsable` (`DateTime`, `DateTimeOffset`, `Guid`, `BigInteger`, `Complex`, `Half`, `Int128`, etc.), [IParsable.TryParse](https://learn.microsoft.com/en-us/dotnet/api/system.iparsable-1.tryparse?view=net-8.0#system-ispanparsable-1-tryparse(system-readonlyspan((system-char))-system-iformatprovider-0@)) or [ISpanParsable.TryParse](https://learn.microsoft.com/en-us/dotnet/api/system.ispanparsable-1.tryparse?view=net-8.0#system-ispanparsable-1-tryparse(system-readonlyspan((system-char))-system-iformatprovider-0@)) is used. +For `enum`, it is parsed using `Enum.TryParse(ignoreCase: true)`. +`bool` is treated as a flag and is always optional. It becomes `true` when the parameter name is passed. +### Array - +Array parsing has three special patterns. +For a regular `T[]`, if the value starts with `[`, it is parsed using `JsonSerialzier.Deserialize`. Otherwise, it is parsed as comma-separated values. For example, `[1,2,3]` or `1,2,3` are allowed as values. To set an empty array, pass `[]`. -`enum` -`nullable?` -`DateTime` +For `params T[]`, all subsequent arguments become the values of the array. For example, if there is an input like `--paramsArray foo bar baz`, it will be bound to a value like `["foo", "bar", "baz"]`. -`ISpanParsable` -#### default -#### json -#### params T[] +### Object +If none of the above cases apply, `JsonSerializer.Deserialize` is used to perform binding as JSON. However, `CancellationToken` and `ConsoleAppContext` are treated as special types and excluded from binding. Also, parameters with the `[FromServices]` attribute are not subject to binding. -#### Custom Value Converter +### Custom Value Converter -// TODO: +To perform custom binding to existing types that do not support `ISpanParsable`, you can create and set up a custom parser. For example, if you want to pass `System.Numerics.Vector3` as a comma-separated string like `1.3,4.12,5.947` and parse it, you can create an `Attribute` with `AttributeTargets.Parameter` that implements `IArgumentParser`'s `static bool TryParse(ReadOnlySpan s, out Vector3 result)` as follows: ```csharp [AttributeUsage(AttributeTargets.Parameter)] @@ -396,34 +507,76 @@ public class Vector3ParserAttribute : Attribute, IArgumentParser } ``` +By setting this attribute on a parameter, the custom parser will be called when parsing the args. +```csharp +ConsoleApp.Run(args, ([Vector3Parser] Vector3 position) => Console.WriteLine(position)); +``` + +### Syntax Parsing Policy and Performance + +While there are some standards for command-line arguments, such as UNIX tools and POSIX, there is no absolute specification. The [Command-line syntax overview for System.CommandLine](https://learn.microsoft.com/en-us/dotnet/standard/commandline/syntax) provides an explanation of the specifications adopted by System.CommandLine. However, ConsoleAppFramework, while referring to these specifications to some extent, does not necessarily aim to fully comply with them. +For example, specifications that change behavior based on `-x` and `-X` or allow bundling `-f -d -x` as `-fdx` are not easy to understand and also take time to parse. The poor performance of System.CommandLine may be influenced by its adherence to complex grammar. Therefore, ConsoleAppFramework prioritizes performance and clear rules. It uses lower-kebab-case as the basis while allowing case-insensitive matching. It does not support ambiguous grammar that cannot be processed in a single pass or takes time to parse. +[System.CommandLine seems to be aiming for a new direction in .NET 9 and .NET 10](https://github.com/dotnet/command-line-api/issues/2338), but from a performance perspective, it will never surpass ConsoleAppFramework. CancellationToken(Gracefully Shutdown) and Timeout --- +In ConsoleAppFramework, when you pass a `CancellationToken` as an argument, it can be used to check for interruption commands (SIGINT/SIGTERM/SIGKILL - Ctrl+C) rather than being treated as a parameter. For handling this, ConsoleAppFramework performs special code generation when a `CancellationToken` is included in the parameters. +```csharp +using var posixSignalHandler = PosixSignalHandler.Register(ConsoleApp.Timeout); +var arg0 = posixSignalHandler.Token; +await Task.Run(() => command(arg0!)).WaitAsync(posixSignalHandler.TimeoutToken); +``` +If a CancellationToken is not passed, the application is immediately forced to terminate when an interruption command (Ctrl+C) is received. However, if a CancellationToken is present, it internally uses [`PosixSignalRegistration`](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.posixsignalregistration) to hook SIGINT/SIGTERM/SIGKILL and sets the CancellationToken to a canceled state. Additionally, it prevents forced termination to allow for a graceful shutdown. +If the CancellationToken is handled correctly, the application can perform proper termination processing based on the application's handling. However, if the CancellationToken is mishandled, the application may not terminate even when an interruption command is received. To avoid this, a timeout timer starts after the interruption command, and the application is forcibly terminated again after the specified time. -Exit Code ---- -If the method returns `int` or `Task` or `ValueTask value, ConsoleAppFramework will set the return value to the exit code. +The default timeout is 5 seconds, but it can be changed using `ConsoleApp.Timeout`. For example, setting it to `ConsoleApp.Timeout = Timeout.InfiniteTimeSpan;` disables the forced termination caused by the timeout. +The hooking behavior using `PosixSignalRegistration` is determined by the presence of a `CancellationToken` (or always takes effect if a filter is set). Therefore, even for synchronous methods, it is possible to change the behavior by including a `CancellationToken` as an argument. +Exit Code +--- +If the method returns `int` or `Task`, `ConsoleAppFramework` will set the return value to the exit code. Due to the nature of code generation, when writing lambda expressions, you need to explicitly specify either `int` or `Task`. -> **NOTE**: If the method throws an unhandled exception, ConsoleAppFramework always set `1` to the exit code. - +```csharp +// return Random ExitCode... +ConsoleApp.Run(args, int () => Random.Shared.Next()); +``` +```csharp +// return StatusCode +await ConsoleApp.RunAsync(args, async Task (string url, CancellationToken cancellationToken) => +{ + using var client = new HttpClient(); + var response = await client.GetAsync(url, cancellationToken); + return (int)response.StatusCode; +}); +``` +If the method throws an unhandled exception, ConsoleAppFramework always set `1` to the exit code. Also, in that case, output `Exception.ToString` to `ConsoleApp.LogError` (the default is `Console.WriteLine`). If you want to modify this code, please create a custom filter. For more details, refer to the [Filter](#filtermiddleware-pipline--consoleappcontext) section. Attribute based parameters validation --- +`ConsoleAppFramework` performs validation when the parameters are marked with attributes for validation from `System.ComponentModel.DataAnnotations` (more precisely, attributes that implement `ValidationAttribute`). The validation occurs after parameter binding and before command execution. If the validation fails, it throws a `ValidationException`. +```csharp +ConsoleApp.Run(args, ([EmailAddress] string firstArg, [Range(0, 2)] int secondArg) => { }); +``` +For example, if you pass arguments like `args = "--first-arg invalid.email --second-arg 10".Split(' ');`, you will see validation failure messages such as: +```txt +The firstArg field is not a valid e-mail address. +The field secondArg must be between 0 and 2. +``` +By default, the ExitCode is set to 1 in this case. Filter(Middleware) Pipline / ConsoleAppContext --- @@ -786,10 +939,15 @@ internal class ServiceProviderScopeFilter(IServiceProvider serviceProvider, Cons Publish to executable file --- +There are multiple ways to run a CLI application in .NET: + +* [dotnet run](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-run) +* [dotnet build](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-build) +* [dotnet publish](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-publish) + +`run` is convenient when you want to execute the `csproj` directly, such as for starting command tools in CI. `build` and `publish` are quite similar, so it's possible to discuss them in general terms, but it's a bit difficult to talk about the precise differences. For more details, it's a good idea to check out [`build` vs `publish` -- can they be friends? · Issue #26247 · dotnet/sdk](https://github.com/dotnet/sdk/issues/26247). -* Native AOT -* dotnet run -* dotnet publish +Also, to run with Native AOT, please refer to the [Native AOT deployment overview](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/). In any case, ConsoleAppFramework thoroughly implements a dependency-free and reflection-free approach, so it shouldn't be an obstacle to execution. License --- diff --git a/sandbox/GeneratorSandbox/Program.cs b/sandbox/GeneratorSandbox/Program.cs index 1f33eb0..d782802 100644 --- a/sandbox/GeneratorSandbox/Program.cs +++ b/sandbox/GeneratorSandbox/Program.cs @@ -6,26 +6,48 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System.ComponentModel.DataAnnotations; +using System.Numerics; using System.Threading.Channels; using ZLogger; -// args = ["--msg", "foobarbaz"]; +args = "--first-arg invalid.email --second-arg 10".Split(' '); -// Microsoft.Extensions.DependencyInjection +ConsoleApp.Timeout = Timeout.InfiniteTimeSpan; -// Package Import: Microsoft.Extensions.Hosting -var builder = Host.CreateApplicationBuilder(); // don't pass args. -using var host = builder.Build(); // using -ConsoleApp.ServiceProvider = host.Services; // use host ServiceProvider -ConsoleApp.Run(args, ([FromServices] ILogger logger) => logger.LogInformation("Hello World!")); +ConsoleApp.Run(args, ( + [Argument] DateTime dateTime, // Argument + [Argument] Guid guidvalue, // + int intVar, // required + bool boolFlag, // flag + MyEnum enumValue, // enum + int[] array, // array + MyClass obj, // object + string optional = "abcde", // optional + double? nullableValue = null, // nullable + params string[] paramsArray // params + ) => { }); + + + + +public enum MyEnum +{ + +} + +public class MyClass +{ + +} + // inject logger public class MyCommand(ILogger logger, IOptions options) { @@ -49,7 +71,32 @@ public class PositionOptions +[AttributeUsage(AttributeTargets.Parameter)] +public class Vector3ParserAttribute : Attribute, IArgumentParser +{ + public static bool TryParse(ReadOnlySpan s, out Vector3 result) + { + Span ranges = stackalloc Range[3]; + var splitCount = s.Split(ranges, ','); + if (splitCount != 3) + { + result = default; + return false; + } + + float x; + float y; + float z; + if (float.TryParse(s[ranges[0]], out x) && float.TryParse(s[ranges[1]], out y) && float.TryParse(s[ranges[2]], out z)) + { + result = new Vector3(x, y, z); + return true; + } + result = default; + return false; + } +} internal class DIFilter(string foo, int bar, ConsoleAppFilter next) diff --git a/sandbox/NativeAot/NativeAot.csproj b/sandbox/NativeAot/NativeAot.csproj new file mode 100644 index 0000000..de8e33a --- /dev/null +++ b/sandbox/NativeAot/NativeAot.csproj @@ -0,0 +1,20 @@ + + + + Exe + net8.0 + enable + enable + + true + true + + + + + Analyzer + false + + + + diff --git a/sandbox/NativeAot/Program.cs b/sandbox/NativeAot/Program.cs new file mode 100644 index 0000000..a37522f --- /dev/null +++ b/sandbox/NativeAot/Program.cs @@ -0,0 +1,3 @@ +using ConsoleAppFramework; + +ConsoleApp.Run(args, (int x, int y) => Console.WriteLine(x + y)); \ No newline at end of file From e473a5c4b7db1a31f793f9e8ac441ad61efd9c80 Mon Sep 17 00:00:00 2001 From: neuecc Date: Mon, 3 Jun 2024 03:22:32 +0900 Subject: [PATCH 54/54] for NuGet --- Icon.png | Bin 0 -> 3185 bytes sandbox/NativeAot/NativeAot.csproj | 1 + .../ConsoleAppFramework.csproj | 24 ++++++++++++++---- 3 files changed, 20 insertions(+), 5 deletions(-) create mode 100644 Icon.png diff --git a/Icon.png b/Icon.png new file mode 100644 index 0000000000000000000000000000000000000000..68d64b1e0e5c10200e85a15ab96a0b43ff989f79 GIT binary patch literal 3185 zcmd5;4NOy46uwx2tpaMaI1$0bD9cb;bOT$#Jv;m%3Mq&bl}?@1ZbI#}yg3*kV$i6I z%n`;CD5ao)fT(j?N@H=+un42n1zIT}0voTZSeaE+qNwO&Ktb}oNr~~ah!x^+s2{b zQJT#p9z!#Dn}bPg-tNur*=7=(f94I8n;Z7DGJ!F;JsR0GpKXCJ2wBF-AI@M@4;CEg z40{3fzuNuRjb6C?pO!`}3iB-|$VuTgYD%^by4@hT@-G9N@-2^|z&EGVUn5#=d|~eF zRN85^^M$yBUuQg8Ct_*YK0(aY`5jN@SabYEyWIyGuZYCG?6TVQaz;j|GH@2-w!vS} zWVLK}CeHJ7gDNzG0T)fb9I1uj#P++D%YZkNm7suPuSHRlb%Nzup_eD~?uGj&wJ_fg z)G@k#LQ6X<&Bwc?^tq(Rv^Z+$miNh5p$fWo`UITPg2_4s z^o`Qg#@gr`aHw~aP&|=ns{=pAf*DMCW~?%BeZukvU*QvtP9+laEd94HE8J8yD$kk% z=>x$#S97ZL9Y@r~1B$sL;Vry@w(yV?P`1onHO*A3ulr>4WNBwZ$r~dTQv;DMnNzot z9vQ4=Ez}sF)M%_c@#{y3T%=lVo>&a9_)J8WjG=*py?t8DVRE{pIXDGnxIj0kM^nCi zB?xr9F8R^a=rdxlCX#BcGo5-ReHdTKN6RCQlChO@?4$P zw{qyVyh2MzDc8b8K-)1`qz3k}H?r8mL>^$4paNB-jx?ac_2OJc^$^CO)ErD4lBxfW zk>Q+AfH{B8#Gy7O0B9hDdw;3xGea=3p$5YggQ*7-yA`J|dL3;XRz2qFDbTUe0R%b= zt7lmXMC^cq8tPe!XScg~kgAI=6`pWs{hfM##3Qy<+xOdEWT-Hq55oxQN~vi<7YnIT z^ZFd`kKK_bcGk^^mzd_o)w`xu34{6Mtz3a`7R7}d;J zy*6U0ki*hzbH1i006}IFs^3w@ksd_eFzJUR^c70-E8Ntf({b#p@X|=Jn7TlNSc2=+ zvus?$jOe)ur;ZGkLf~MKyp3uB@uh@ot<9PE4sI0P&s#}*b9a4(JX5gEkgw6D^DG?; nKbDmhcKT)eOmzPo{ZI~Qz{z@ji^5RskI;mzjtH%0Z_oS(cSy02 literal 0 HcmV?d00001 diff --git a/sandbox/NativeAot/NativeAot.csproj b/sandbox/NativeAot/NativeAot.csproj index de8e33a..d6775be 100644 --- a/sandbox/NativeAot/NativeAot.csproj +++ b/sandbox/NativeAot/NativeAot.csproj @@ -5,6 +5,7 @@ net8.0 enable enable + false true true diff --git a/src/ConsoleAppFramework/ConsoleAppFramework.csproj b/src/ConsoleAppFramework/ConsoleAppFramework.csproj index b6c00c5..f5d50dc 100644 --- a/src/ConsoleAppFramework/ConsoleAppFramework.csproj +++ b/src/ConsoleAppFramework/ConsoleAppFramework.csproj @@ -1,6 +1,4 @@ - - - + netstandard2.0 @@ -8,9 +6,18 @@ enable enable ConsoleAppFramework - true cs + + + false + true + false + true + + + ConsoleAppFramework + Zero Dependency, Zero Overhead, Zero Reflection, Zero Allocation, AOT Safe CLI Framework powered by C# Source Generator. @@ -21,6 +28,13 @@
- + + + + + + + +