├── Icon.png
├── global.json
├── .dockerignore
├── sandbox
├── GeneratorSandbox
│ ├── appsettings.json
│ ├── Program.cs
│ ├── Properties
│ │ └── launchSettings.json
│ ├── GeneratorSandbox.csproj
│ ├── Temp.cs
│ └── Filters.cs
├── CliFrameworkBenchmark
│ ├── README.md
│ ├── Properties
│ │ └── launchSettings.json
│ ├── Commands
│ │ ├── CoconaCommand.cs
│ │ ├── CommandLineParserCommand.cs
│ │ ├── CliprCommand.cs
│ │ ├── McMasterCommand.cs
│ │ ├── PowerArgsCommand.cs
│ │ ├── CliFxCommand.cs
│ │ ├── SpectreConsoleCliCommand.cs
│ │ ├── ConsoleAppFrameworkCommand.cs
│ │ └── SystemCommandLineCommand.cs
│ ├── Program.cs
│ ├── CliFrameworkBenchmark.csproj
│ └── Benchmark.cs
├── NativeAot
│ ├── Program.cs
│ └── NativeAot.csproj
└── FilterShareProject
│ ├── FilterShareProject.csproj
│ └── Class1.cs
├── src
├── ConsoleAppFramework
│ ├── Properties
│ │ └── launchSettings.json
│ ├── SourceGeneratorContexts.cs
│ ├── IgnoreEquality.cs
│ ├── StringExtensions.cs
│ ├── NameConverter.cs
│ ├── EquatableArray.cs
│ ├── ConsoleAppFramework.csproj
│ ├── EquatableTypeSymbol.cs
│ ├── WellKnownTypes.cs
│ ├── SourceBuilder.cs
│ ├── DiagnosticDescriptors.cs
│ ├── RoslynExtensions.cs
│ └── CommandHelpBuilder.cs
├── ConsoleAppFramework.Abstractions
│ ├── ConsoleAppFramework.Abstractions.props
│ ├── ConsoleAppFramework.Abstractions.csproj
│ └── ConsoleApp.Abstractions.cs
└── ConsoleAppFramework.CliSchema
│ ├── ConsoleAppFramework.CliSchema.csproj
│ └── CommandHelpDefinition.cs
├── .github
├── workflows
│ ├── stale.yaml
│ ├── build-debug.yaml
│ └── build-release.yaml
└── dependabot.yaml
├── exclusion.dic
├── tests
├── ConsoleAppFramework.GeneratorTests
│ ├── ConsoleAppFramework.GeneratorTests.csproj
│ ├── RegisterCommandsTest.cs
│ ├── GeneratorOptionsTest.cs
│ ├── ArrayParseTest.cs
│ ├── NameConverterTest.cs
│ ├── DITest.cs
│ ├── ArgumentParserTest.cs
│ ├── BuildCustomDelegateTest.cs
│ ├── ConsoleAppContextTest.cs
│ ├── HiddenAttributeTest.cs
│ ├── SubCommandTest.cs
│ ├── FilterTest.cs
│ ├── GlobalOptionTest.cs
│ ├── ConsoleAppBuilderTest.cs
│ ├── RunTest.cs
│ ├── CSharpGeneratorRunner.cs
│ ├── HelpTest.cs
│ └── DiagnosticsTest.cs
└── ConsoleAppFramework.NativeAotTests
│ ├── ConsoleAppFramework.NativeAotTests.csproj
│ └── NativeAotTest.cs
├── Directory.Build.props
├── LICENSE
├── .editorconfig
├── ConsoleAppFramework.slnx
└── .gitignore
/Icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cysharp/ConsoleAppFramework/HEAD/Icon.png
--------------------------------------------------------------------------------
/global.json:
--------------------------------------------------------------------------------
1 | {
2 | "test": {
3 | "runner": "Microsoft.Testing.Platform"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | .dockerignore
2 | .env
3 | .git
4 | .gitignore
5 | .vs
6 | .vscode
7 | docker-compose.yml
8 | docker-compose.*.yml
9 | */bin
10 | */obj
11 |
--------------------------------------------------------------------------------
/sandbox/GeneratorSandbox/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Position": {
3 | "Title": "Editor",
4 | "Name": "Joe Smith"
5 | },
6 | "MyKey": "My appsettings.json Value",
7 | "AllowedHosts": "*"
8 | }
--------------------------------------------------------------------------------
/sandbox/CliFrameworkBenchmark/README.md:
--------------------------------------------------------------------------------
1 | This benchmark project is based on CliFx.Benchmarks and Cocona Benchmarks.
2 |
3 | https://github.com/Tyrrrz/CliFx/tree/master/CliFx.Benchmarks/
4 |
5 | https://github.com/mayuki/Cocona/tree/master/perf/Cocona.Benchmark.External
6 |
--------------------------------------------------------------------------------
/src/ConsoleAppFramework/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "Roslyn Debug Profile": {
4 | "commandName": "DebugRoslynComponent",
5 | "targetProject": "..\\..\\sandbox\\GeneratorSandbox\\GeneratorSandbox.csproj"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/ConsoleAppFramework.Abstractions/ConsoleAppFramework.Abstractions.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | USE_EXTERNAL_CONSOLEAPP_ABSTRACTIONS
4 |
5 |
6 |
--------------------------------------------------------------------------------
/sandbox/GeneratorSandbox/Program.cs:
--------------------------------------------------------------------------------
1 | using ConsoleAppFramework;
2 |
3 | args = ["write", "--help"];
4 |
5 | var app = ConsoleApp.Create();
6 |
7 | app.Add("write", (Target target) => { });
8 |
9 | app.Run(args);
10 |
11 | public enum Target
12 | {
13 | File,
14 | Network
15 | }
16 |
--------------------------------------------------------------------------------
/sandbox/CliFrameworkBenchmark/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "Default": {
4 | "commandName": "Project",
5 | "commandLineArgs": ""
6 | },
7 | "Measure": {
8 | "commandName": "Project",
9 | "commandLineArgs": "--launchCount 20"
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/.github/workflows/stale.yaml:
--------------------------------------------------------------------------------
1 | name: "Close stale issues"
2 |
3 | on:
4 | workflow_dispatch:
5 | schedule:
6 | - cron: "0 0 * * *"
7 |
8 | jobs:
9 | stale:
10 | permissions:
11 | contents: read
12 | pull-requests: write
13 | issues: write
14 | uses: Cysharp/Actions/.github/workflows/stale-issue.yaml@main
15 |
--------------------------------------------------------------------------------
/src/ConsoleAppFramework/SourceGeneratorContexts.cs:
--------------------------------------------------------------------------------
1 | namespace ConsoleAppFramework;
2 |
3 | readonly record struct ConsoleAppFrameworkGeneratorOptions(bool DisableNamingConversion);
4 |
5 | readonly record struct DllReference(bool HasDependencyInjection, bool HasLogging, bool HasConfiguration, bool HasJsonConfiguration, bool HasHost, bool HasCliSchema);
6 |
--------------------------------------------------------------------------------
/sandbox/NativeAot/Program.cs:
--------------------------------------------------------------------------------
1 | using ConsoleAppFramework;
2 | using System.Diagnostics.CodeAnalysis;
3 | using System.Runtime.CompilerServices;
4 | using System.Text.Json;
5 |
6 | var app = ConsoleApp.Create();
7 |
8 | ConsoleApp.Run(args, (int x, Kabayaki y) => Console.WriteLine(x + y.MyProperty));
9 |
10 | app.Run(args);
11 |
12 | public class Kabayaki
13 | {
14 | public int MyProperty { get; set; }
15 | }
16 |
17 |
--------------------------------------------------------------------------------
/sandbox/CliFrameworkBenchmark/Commands/CoconaCommand.cs:
--------------------------------------------------------------------------------
1 | namespace CliFrameworkBenchmarks.Commands;
2 |
3 | public class CoconaCommand
4 | {
5 | public void Execute(
6 | [Cocona.Option("str", new []{'s'})]
7 | string? strOption,
8 | [Cocona.Option("int", new []{'i'})]
9 | int intOption,
10 | [Cocona.Option("bool", new []{'b'})]
11 | bool boolOption)
12 | {
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/sandbox/CliFrameworkBenchmark/Commands/CommandLineParserCommand.cs:
--------------------------------------------------------------------------------
1 | namespace CliFrameworkBenchmarks.Commands;
2 |
3 | public class CommandLineParserCommand
4 | {
5 | [CommandLine.Option('s', "str")]
6 | public string? StrOption { get; set; }
7 |
8 | [CommandLine.Option('i', "int")]
9 | public int IntOption { get; set; }
10 |
11 | [CommandLine.Option('b', "bool")]
12 | public bool BoolOption { get; set; }
13 |
14 | public void Execute()
15 | {
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/sandbox/FilterShareProject/FilterShareProject.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 | Debug;Release
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/sandbox/CliFrameworkBenchmark/Commands/CliprCommand.cs:
--------------------------------------------------------------------------------
1 | using clipr;
2 |
3 | namespace CliFrameworkBenchmarks.Commands;
4 |
5 | public class CliprCommand
6 | {
7 | [NamedArgument('s', "str")]
8 | public string? StrOption { get; set; }
9 |
10 | [NamedArgument('i', "int")]
11 | public int IntOption { get; set; }
12 |
13 | [NamedArgument('b', "bool", Constraint = NumArgsConstraint.Optional, Const = true)]
14 | public bool BoolOption { get; set; }
15 |
16 | public void Execute()
17 | {
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/sandbox/CliFrameworkBenchmark/Commands/McMasterCommand.cs:
--------------------------------------------------------------------------------
1 | namespace CliFrameworkBenchmarks.Commands;
2 |
3 | public class McMasterCommand
4 | {
5 | [McMaster.Extensions.CommandLineUtils.Option("--str|-s")]
6 | public string? StrOption { get; set; }
7 |
8 | [McMaster.Extensions.CommandLineUtils.Option("--int|-i")]
9 | public int IntOption { get; set; }
10 |
11 | [McMaster.Extensions.CommandLineUtils.Option("--bool|-b")]
12 | public bool BoolOption { get; set; }
13 |
14 | public int OnExecute() => 0;
15 | }
16 |
--------------------------------------------------------------------------------
/sandbox/GeneratorSandbox/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "Default": {
4 | "commandName": "Project",
5 | "commandLineArgs": ""
6 | },
7 | "ShowVersion": {
8 | "commandName": "Project",
9 | "commandLineArgs": "--version"
10 | },
11 | "ShowRootCommandHelp": {
12 | "commandName": "Project",
13 | "commandLineArgs": "--help"
14 | },
15 | "ShowRunCommandHelp": {
16 | "commandName": "Project",
17 | "commandLineArgs": "run --help"
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/exclusion.dic:
--------------------------------------------------------------------------------
1 | abcd
2 | abcde
3 | abcdefg
4 | aiueo
5 | appsettings
6 | args
7 | authed
8 | awaitable
9 | Awaiter
10 | Binded
11 | Clipr
12 | Cysharp
13 | Decr
14 | dest
15 | Equatable
16 | fooa
17 | foobarbaz
18 | generatortest
19 | hoge
20 | ignorecase
21 | Impl
22 | Incr
23 | Kabayaki
24 | Kokuban
25 | Lamda
26 | Moge
27 | nomsg
28 | nomunomu
29 | Numerics
30 | Parsable
31 | posix
32 | saas
33 | Spectre
34 | stackalloc
35 | stdout
36 | Tacommands
37 | tako
38 | takoyaki
39 | Withargs
40 | Yaki
41 | Zeroargs
42 |
--------------------------------------------------------------------------------
/sandbox/CliFrameworkBenchmark/Commands/PowerArgsCommand.cs:
--------------------------------------------------------------------------------
1 | using PowerArgs;
2 |
3 | namespace CliFrameworkBenchmarks.Commands;
4 |
5 | //public class PowerArgsCommand
6 | //{
7 | // [ArgShortcut("--str"), ArgShortcut("-s")]
8 | // public string? StrOption { get; set; }
9 |
10 | // [ArgShortcut("--int"), ArgShortcut("-i")]
11 | // public int IntOption { get; set; }
12 |
13 | // [ArgShortcut("--bool"), ArgShortcut("-b")]
14 | // public bool BoolOption { get; set; }
15 |
16 | // public void Main()
17 | // {
18 | // }
19 | //}
20 |
--------------------------------------------------------------------------------
/sandbox/FilterShareProject/Class1.cs:
--------------------------------------------------------------------------------
1 | using ConsoleAppFramework;
2 |
3 | namespace FilterShareProject;
4 |
5 | public class TakoFilter(ConsoleAppFilter next) : ConsoleAppFilter(next)
6 | {
7 | public override Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken)
8 | {
9 | Console.WriteLine("TAKO");
10 | return Next.InvokeAsync(context, cancellationToken);
11 | }
12 | }
13 |
14 | public class OtherProjectCommand
15 | {
16 | public void Execute(int x)
17 | {
18 | Console.WriteLine("Hello?");
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/sandbox/CliFrameworkBenchmark/Commands/CliFxCommand.cs:
--------------------------------------------------------------------------------
1 | using CliFx.Attributes;
2 | using CliFx.Infrastructure;
3 |
4 | namespace CliFrameworkBenchmarks.Commands;
5 |
6 | [Command]
7 | public class CliFxCommand : CliFx.ICommand
8 | {
9 | [CommandOption("str", 's')]
10 | public string? StrOption { get; set; }
11 |
12 | [CommandOption("int", 'i')]
13 | public int IntOption { get; set; }
14 |
15 | [CommandOption("bool", 'b')]
16 | public bool BoolOption { get; set; }
17 |
18 | public ValueTask ExecuteAsync(IConsole console) => ValueTask.CompletedTask;
19 | }
20 |
--------------------------------------------------------------------------------
/src/ConsoleAppFramework.CliSchema/ConsoleAppFramework.CliSchema.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0;net10.0
5 | enable
6 | enable
7 | latest
8 |
9 |
10 | true
11 | ConsoleAppFramework.CliSchema
12 | ConsoleAppFramework cli-schema metadata library.
13 | Debug;Release
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/ConsoleAppFramework/IgnoreEquality.cs:
--------------------------------------------------------------------------------
1 | namespace ConsoleAppFramework;
2 |
3 | public readonly struct IgnoreEquality(T value) : IEquatable>
4 | {
5 | public readonly T Value => value;
6 |
7 | public static implicit operator IgnoreEquality(T value)
8 | {
9 | return new IgnoreEquality(value);
10 | }
11 |
12 | public static implicit operator T(IgnoreEquality value)
13 | {
14 | return value.Value;
15 | }
16 |
17 | public bool Equals(IgnoreEquality other)
18 | {
19 | // always true to ignore equality check.
20 | return true;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/ConsoleAppFramework/StringExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace ConsoleAppFramework;
2 |
3 | internal static class StringExtensions
4 | {
5 | #if NETSTANDARD2_0
6 | public static string ReplaceLineEndings(this string input)
7 | {
8 | #pragma warning disable RS1035
9 | return ReplaceLineEndings(input, Environment.NewLine);
10 | #pragma warning restore RS1035
11 | }
12 |
13 | public static string ReplaceLineEndings(this string text, string replacementText)
14 | {
15 | text = text.Replace("\r\n", "\n");
16 |
17 | if (replacementText != "\n")
18 | text = text.Replace("\n", replacementText);
19 |
20 | return text;
21 | }
22 | #endif
23 | }
24 |
--------------------------------------------------------------------------------
/.github/dependabot.yaml:
--------------------------------------------------------------------------------
1 | # ref: https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
2 | version: 2
3 | updates:
4 | - package-ecosystem: "github-actions"
5 | directory: "/"
6 | schedule:
7 | interval: "weekly" # Check for updates to GitHub Actions every week
8 | cooldown:
9 | default-days: 14 # Wait 14 days before creating another PR for the same dependency. This will prevent vulnerability on the package impact.
10 | ignore:
11 | # I just want update action when major/minor version is updated. patch updates are too noisy.
12 | - dependency-name: "*"
13 | update-types:
14 | - version-update:semver-patch
15 |
--------------------------------------------------------------------------------
/src/ConsoleAppFramework.Abstractions/ConsoleAppFramework.Abstractions.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0;net10.0
5 | enable
6 | enable
7 |
8 |
9 | true
10 | ConsoleAppFramework.Abstractions
11 | ConsoleAppFramework external abstractions library.
12 | Debug;Release
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/sandbox/CliFrameworkBenchmark/Commands/SpectreConsoleCliCommand.cs:
--------------------------------------------------------------------------------
1 | using Spectre.Console.Cli;
2 |
3 | namespace CliFrameworkBenchmarks.Commands;
4 |
5 | public class SpectreConsoleCliCommand : Command
6 | {
7 | public sealed class Settings : CommandSettings
8 | {
9 | [CommandOption("-s")]
10 | public string? strOption { get; init; }
11 |
12 | [CommandOption("-i")]
13 | public int intOption { get; init; }
14 |
15 | [CommandOption("-b")]
16 | public bool boolOption { get; init; }
17 | }
18 |
19 | public override int Execute(CommandContext context, Settings settings, CancellationToken cancellationToken)
20 | {
21 | return 0;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppFramework.GeneratorTests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net10.0
6 | enable
7 | enable
8 | true
9 | Debug;Release
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/sandbox/NativeAot/NativeAot.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net9.0
6 | enable
7 | enable
8 |
9 | true
10 | true
11 | true
12 | true
13 | true
14 |
15 | Debug;Release
16 |
17 |
18 |
19 |
20 | Analyzer
21 | false
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/ConsoleAppFramework/NameConverter.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 |
3 | namespace ConsoleAppFramework;
4 |
5 | public static class NameConverter
6 | {
7 | public static string ToKebabCase(string name)
8 | {
9 | var sb = new StringBuilder();
10 | for (int i = 0; i < name.Length; i++)
11 | {
12 | if (!Char.IsUpper(name[i]))
13 | {
14 | sb.Append(name[i]);
15 | continue;
16 | }
17 |
18 | // Abc, abC, AB-c => first or Last or capital continuous, no added.
19 | if (i == 0 || i == name.Length - 1 || Char.IsUpper(name[i + 1]))
20 | {
21 | sb.Append(Char.ToLowerInvariant(name[i]));
22 | continue;
23 | }
24 |
25 | // others, add-
26 | sb.Append('-');
27 | sb.Append(Char.ToLowerInvariant(name[i]));
28 | }
29 | return sb.ToString();
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/.github/workflows/build-debug.yaml:
--------------------------------------------------------------------------------
1 | name: Build-Debug
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | branches:
7 | - "master"
8 | pull_request:
9 | branches:
10 | - master
11 |
12 | jobs:
13 | build-dotnet:
14 | permissions:
15 | contents: read
16 | runs-on: ubuntu-24.04
17 | timeout-minutes: 10
18 | steps:
19 | - uses: Cysharp/Actions/.github/actions/checkout@main
20 | - uses: Cysharp/Actions/.github/actions/setup-dotnet@main
21 | - run: dotnet build -c Release
22 | - run: dotnet test -c Release --no-build
23 |
24 | # Native AOT tests
25 | - run: dotnet publish -r linux-x64 tests/ConsoleAppFramework.NativeAotTests/ConsoleAppFramework.NativeAotTests.csproj
26 | - run: tests/ConsoleAppFramework.NativeAotTests/bin/Release/net10.0/linux-x64/publish/ConsoleAppFramework.NativeAotTests
27 |
28 | - run: dotnet pack -c Release --no-build -p:IncludeSymbols=true -o $GITHUB_WORKSPACE/artifacts
29 |
--------------------------------------------------------------------------------
/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | false
5 | $(Version)
6 | Cysharp
7 | Cysharp
8 | © Cysharp, Inc.
9 | https://github.com/Cysharp/ConsoleAppFramework
10 | README.md
11 | $(PackageProjectUrl)
12 | git
13 | batch,console,cli,consoleappframework
14 | MIT
15 | Icon.png
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/tests/ConsoleAppFramework.NativeAotTests/ConsoleAppFramework.NativeAotTests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net10.0
6 | enable
7 | enable
8 | true
9 | true
10 | true
11 | true
12 | true
13 | false
14 | true
15 |
16 |
17 |
18 |
19 |
20 | Analyzer
21 | false
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Cysharp, Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # top-most EditorConfig file
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | end_of_line = lf
7 | indent_style = space
8 | indent_size = 2
9 | insert_final_newline = true
10 | trim_trailing_whitespace = true
11 |
12 | # Visual Studio Spell checker configs (https://learn.microsoft.com/en-us/visualstudio/ide/text-spell-checker?view=vs-2022#how-to-customize-the-spell-checker)
13 | spelling_exclusion_path = ./exclusion.dic
14 |
15 | [*.cs]
16 | indent_size = 4
17 | charset = utf-8-bom
18 | end_of_line = unset
19 |
20 | # Solution files
21 | [*.{sln,slnx}]
22 | end_of_line = unset
23 |
24 | # MSBuild project files
25 | [*.{csproj,props,targets}]
26 | end_of_line = unset
27 |
28 | # Xml config files
29 | [*.{ruleset,config,nuspec,resx,runsettings,DotSettings}]
30 | end_of_line = unset
31 |
32 | # C# code style settings
33 | [*.{cs}]
34 | dotnet_diagnostic.IDE0044.severity = none # IDE0044: Make field readonly
35 |
36 | # https://stackoverflow.com/questions/79195382/how-to-disable-fading-unused-methods-in-visual-studio-2022-17-12-0
37 | dotnet_diagnostic.IDE0051.severity = none # IDE0051: Remove unused private member
38 | dotnet_diagnostic.IDE0130.severity = none # IDE0130: Namespace does not match folder structure
39 |
--------------------------------------------------------------------------------
/ConsoleAppFramework.slnx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/sandbox/CliFrameworkBenchmark/Commands/ConsoleAppFrameworkCommand.cs:
--------------------------------------------------------------------------------
1 | namespace CliFrameworkBenchmarks.Commands;
2 | //public class ConsoleAppFrameworkCommand : ConsoleAppBase
3 | //{
4 | // public void Execute(
5 | // [global::ConsoleAppFramework.Option("s")]
6 | // string? str,
7 | // [global::ConsoleAppFramework.Option("i")]
8 | // int intOption,
9 | // [global::ConsoleAppFramework.Option("b")]
10 | // bool boolOption)
11 | // {
12 | // }
13 | //}
14 |
15 | public class ConsoleAppFrameworkCommand
16 | {
17 | ///
18 | ///
19 | ///
20 | /// -s
21 | /// -i
22 | /// -b
23 | public static void Execute(string? str, int intOption, bool boolOption)
24 | {
25 |
26 | }
27 |
28 | ///
29 | ///
30 | ///
31 | /// -s
32 | /// -i
33 | /// -b
34 | public static Task ExecuteWithCancellationToken(string? str, int intOption, bool boolOption, CancellationToken cancellationToken)
35 | {
36 | return Task.CompletedTask;
37 | }
38 | }
39 |
40 | //internal class NopConsoleAppFilter(ConsoleAppFilter next) : ConsoleAppFilter(next)
41 | //{
42 | // public override Task InvokeAsync(CancellationToken cancellationToken)
43 | // {
44 | // return Next.InvokeAsync(cancellationToken);
45 | // }
46 | //}
47 |
--------------------------------------------------------------------------------
/src/ConsoleAppFramework/EquatableArray.cs:
--------------------------------------------------------------------------------
1 | using System.Collections;
2 | using System.Runtime.CompilerServices;
3 |
4 | namespace ConsoleAppFramework;
5 |
6 | public readonly struct EquatableArray : IEquatable>, IEnumerable
7 | where T : IEquatable
8 | {
9 | readonly T[]? array;
10 |
11 | public EquatableArray() // for collection literal []
12 | {
13 | array = [];
14 | }
15 |
16 | public EquatableArray(T[] array)
17 | {
18 | this.array = array;
19 | }
20 |
21 | public static implicit operator EquatableArray(T[] array)
22 | {
23 | return new EquatableArray(array);
24 | }
25 |
26 | public ref readonly T this[int index]
27 | {
28 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
29 | get => ref array![index];
30 | }
31 |
32 | public int Length => array!.Length;
33 |
34 | public ReadOnlySpan AsSpan()
35 | {
36 | return array.AsSpan();
37 | }
38 |
39 | public ReadOnlySpan.Enumerator GetEnumerator()
40 | {
41 | return AsSpan().GetEnumerator();
42 | }
43 |
44 | IEnumerator IEnumerable.GetEnumerator()
45 | {
46 | return array.AsEnumerable().GetEnumerator();
47 | }
48 |
49 | IEnumerator IEnumerable.GetEnumerator()
50 | {
51 | return array.AsEnumerable().GetEnumerator();
52 | }
53 |
54 | public bool Equals(EquatableArray other)
55 | {
56 | return AsSpan().SequenceEqual(other.AsSpan());
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/sandbox/CliFrameworkBenchmark/Commands/SystemCommandLineCommand.cs:
--------------------------------------------------------------------------------
1 | using System.CommandLine;
2 |
3 | namespace CliFrameworkBenchmarks.Commands;
4 |
5 | public class SystemCommandLineCommand
6 | {
7 | public static int ParseInvoke(string[] args)
8 | {
9 | var stringOption = new Option("--str", "-s");
10 | var intOption = new Option("--int", "-i");
11 | var boolOption = new Option("--bool", "-b");
12 |
13 | var command = new RootCommand { stringOption, intOption, boolOption };
14 |
15 | command.SetAction(parseResult =>
16 | {
17 | _ = parseResult.GetValue(stringOption);
18 | _ = parseResult.GetValue(intOption);
19 | _ = parseResult.GetValue(boolOption);
20 | });
21 |
22 | return command.Parse(args).Invoke();
23 | }
24 |
25 | public static Task ParseInvokeAsync(string[] args)
26 | {
27 | var stringOption = new Option("--str", "-s");
28 | var intOption = new Option("--int", "-i");
29 | var boolOption = new Option("--bool", "-b");
30 |
31 | var command = new RootCommand { stringOption, intOption, boolOption };
32 |
33 | command.SetAction((parseResult, cancellationToken) =>
34 | {
35 | _ = parseResult.GetValue(stringOption);
36 | _ = parseResult.GetValue(intOption);
37 | _ = parseResult.GetValue(boolOption);
38 | return Task.CompletedTask;
39 | });
40 |
41 | return command.Parse(args).InvokeAsync();
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/sandbox/CliFrameworkBenchmark/Program.cs:
--------------------------------------------------------------------------------
1 | // This benchmark project is based on CliFx.Benchmarks.
2 | // https://github.com/Tyrrrz/CliFx/tree/master/CliFx.Benchmarks/
3 |
4 | using BenchmarkDotNet.Configs;
5 | using BenchmarkDotNet.Diagnosers;
6 | using BenchmarkDotNet.Engines;
7 | using BenchmarkDotNet.Jobs;
8 | using BenchmarkDotNet.Reports;
9 | using BenchmarkDotNet.Running;
10 | using BenchmarkDotNet.Toolchains.CsProj;
11 | using Perfolizer.Horology;
12 |
13 | namespace CliFrameworkBenchmarks;
14 |
15 | class Program
16 | {
17 | static void Main(string[] args)
18 | {
19 | var config = DefaultConfig.Instance
20 | .WithSummaryStyle(SummaryStyle.Default
21 | .WithTimeUnit(TimeUnit.Millisecond))
22 | .HideColumns(BenchmarkDotNet.Columns.Column.Error)
23 | ;
24 |
25 | config.AddDiagnoser(MemoryDiagnoser.Default);
26 | // config.AddDiagnoser(new ThreadingDiagnoser(new ThreadingDiagnoserConfig(displayLockContentionWhenZero: false, displayCompletedWorkItemCountWhenZero: false)));
27 |
28 | config.AddJob(Job.Default
29 | .WithStrategy(RunStrategy.ColdStart)
30 | .WithLaunchCount(1)
31 | .WithWarmupCount(0)
32 | .WithIterationCount(1)
33 | .WithInvocationCount(1)
34 | .WithToolchain(CsProjCoreToolchain.NetCoreApp10_0) // .NET 10
35 | .DontEnforcePowerPlan());
36 |
37 | BenchmarkRunner.Run(config, args);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/sandbox/CliFrameworkBenchmark/CliFrameworkBenchmark.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net10.0
6 | enable
7 | 14
8 | annotations
9 | true
10 | Debug;Release
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | Analyzer
36 | false
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/.github/workflows/build-release.yaml:
--------------------------------------------------------------------------------
1 | name: Build-Release
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | tag:
7 | description: "tag: git tag you want create. (sample 1.0.0)"
8 | required: true
9 | dry-run:
10 | description: "dry-run: true will never create release/nuget."
11 | required: true
12 | default: false
13 | type: boolean
14 |
15 | jobs:
16 | build-dotnet:
17 | permissions:
18 | contents: read
19 | runs-on: ubuntu-24.04
20 | timeout-minutes: 10
21 | steps:
22 | - uses: Cysharp/Actions/.github/actions/checkout@main
23 | - uses: Cysharp/Actions/.github/actions/setup-dotnet@main
24 | - run: dotnet build -c Release -p:Version=${{ inputs.tag }}
25 | - run: dotnet test -c Release --no-build
26 | # Native AOT tests
27 | - run: dotnet publish -r linux-x64 tests/ConsoleAppFramework.NativeAotTests/ConsoleAppFramework.NativeAotTests.csproj
28 | - run: tests/ConsoleAppFramework.NativeAotTests/bin/Release/net10.0/linux-x64/publish/ConsoleAppFramework.NativeAotTests
29 | # pack nuget
30 | - run: dotnet pack -c Release --no-build -p:Version=${{ inputs.tag }} -o ./publish
31 | - uses: Cysharp/Actions/.github/actions/upload-artifact@main
32 | with:
33 | name: nuget
34 | path: ./publish
35 | retention-days: 1
36 |
37 | # release
38 | create-release:
39 | needs: [build-dotnet]
40 | permissions:
41 | contents: write
42 | id-token: write # required for NuGet Trusted Publish
43 | uses: Cysharp/Actions/.github/workflows/create-release.yaml@main
44 | with:
45 | commit-id: ${{ github.sha }}
46 | dry-run: ${{ inputs.dry-run }}
47 | tag: ${{ inputs.tag }}
48 | nuget-push: true
49 | secrets: inherit
50 |
--------------------------------------------------------------------------------
/tests/ConsoleAppFramework.GeneratorTests/RegisterCommandsTest.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace ConsoleAppFramework.GeneratorTests;
8 |
9 | public class RegisterCommandsTest
10 | {
11 | readonly VerifyHelper verifier = new("CAF");
12 |
13 | [Test]
14 | public async Task VerifyDuplicate()
15 | {
16 | await verifier.Verify(7, """
17 | var app = ConsoleApp.Create();
18 | app.Run(args);
19 |
20 | [RegisterCommands]
21 | public class Foo
22 | {
23 | public async Task Bar(int x)
24 | {
25 | Console.Write(x);
26 | }
27 |
28 | public async Task Baz(int y)
29 | {
30 | Console.Write(y);
31 | }
32 | }
33 |
34 | [RegisterCommands]
35 | public class Hoge
36 | {
37 | public async Task Bar(int x)
38 | {
39 | Console.Write(x);
40 | }
41 |
42 | public async Task Baz(int y)
43 | {
44 | Console.Write(y);
45 | }
46 | }
47 | """, "Bar");
48 | }
49 |
50 | [Test]
51 | public async Task Exec()
52 | {
53 | var code = """
54 | var app = ConsoleApp.Create();
55 | app.Run(args);
56 |
57 | [RegisterCommands]
58 | public class Foo
59 | {
60 | public async Task Bar(int x)
61 | {
62 | Console.Write(x);
63 | }
64 |
65 | public async Task Baz(int y)
66 | {
67 | Console.Write(y);
68 | }
69 | }
70 |
71 | [RegisterCommands("hoge")]
72 | public class Hoge
73 | {
74 | public async Task Bar(int x)
75 | {
76 | Console.Write(x);
77 | }
78 |
79 | public async Task Baz(int y)
80 | {
81 | Console.Write(y);
82 | }
83 | }
84 | """;
85 |
86 | await verifier.Execute(code, "bar --x 10", "10");
87 | await verifier.Execute(code, "baz --y 20", "20");
88 | await verifier.Execute(code, "hoge bar --x 10", "10");
89 | await verifier.Execute(code, "hoge baz --y 20", "20");
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/tests/ConsoleAppFramework.GeneratorTests/GeneratorOptionsTest.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace ConsoleAppFramework.GeneratorTests;
8 |
9 | public class GeneratorOptionsTest
10 | {
11 | VerifyHelper verifier = new VerifyHelper("CAF");
12 |
13 | [Test]
14 | public async Task DisableNamingConversionRun()
15 | {
16 | await verifier.Execute("""
17 | [assembly: ConsoleAppFrameworkGeneratorOptions(DisableNamingConversion = true)]
18 |
19 | ConsoleApp.Run(args, (int fooBarBaz) => { Console.Write(fooBarBaz); });
20 | """, args: "--fooBarBaz 100", expected: "100");
21 |
22 | await verifier.Execute("""
23 | [assembly: ConsoleAppFrameworkGeneratorOptions(DisableNamingConversion = false)]
24 |
25 | ConsoleApp.Run(args, (int fooBarBaz) => { Console.Write(fooBarBaz); });
26 | """, args: "--foo-bar-baz 100", expected: "100");
27 | }
28 |
29 | [Test]
30 | public async Task DisableNamingConversionBuilder()
31 | {
32 | await verifier.Execute("""
33 | [assembly: ConsoleAppFrameworkGeneratorOptions(DisableNamingConversion = true)]
34 |
35 | var app = ConsoleApp.Create();
36 | app.Add();
37 | app.Run(args);
38 |
39 | class Commands
40 | {
41 | public async Task FooBarBaz(int hogeMoge, int takoYaki)
42 | {
43 | Console.Write(hogeMoge + takoYaki);
44 | }
45 | }
46 | """, args: "FooBarBaz --hogeMoge 100 --takoYaki 200", expected: "300");
47 |
48 | await verifier.Execute("""
49 | [assembly: ConsoleAppFrameworkGeneratorOptions(DisableNamingConversion = false)]
50 |
51 | var app = ConsoleApp.Create();
52 | app.Add();
53 | app.Run(args);
54 |
55 | class Commands
56 | {
57 | public async Task FooBarBaz(int hogeMoge, int takoYaki)
58 | {
59 | Console.Write(hogeMoge + takoYaki);
60 | }
61 | }
62 | """, args: "foo-bar-baz --hoge-moge 100 --tako-yaki 200", expected: "300");
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/ConsoleAppFramework/ConsoleAppFramework.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | 12
6 | enable
7 | enable
8 | ConsoleAppFramework
9 | true
10 | cs
11 |
12 |
13 | false
14 | true
15 | false
16 | true
17 | true
18 |
19 |
20 | true
21 | ConsoleAppFramework
22 | Zero Dependency, Zero Overhead, Zero Reflection, Zero Allocation, AOT Safe CLI Framework powered by C# Source Generator.
23 | Debug;Release
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | all
38 | runtime; build; native; contentfiles; analyzers; buildtransitive
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/src/ConsoleAppFramework.CliSchema/CommandHelpDefinition.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 |
3 | namespace ConsoleAppFramework;
4 |
5 | public record CommandHelpDefinition
6 | {
7 | public string CommandName { get; }
8 | public CommandOptionHelpDefinition[] Options { get; }
9 | public string Description { get; }
10 |
11 | public CommandHelpDefinition(string commandName, CommandOptionHelpDefinition[] options, string description)
12 | {
13 | CommandName = commandName;
14 | Options = options;
15 | Description = description;
16 | }
17 | }
18 |
19 | public record CommandOptionHelpDefinition
20 | {
21 | public string[] Options { get; }
22 | public string Description { get; }
23 | public string? DefaultValue { get; }
24 | public string ValueTypeName { get; }
25 | public int? Index { get; }
26 | public bool IsRequired => DefaultValue == null && !IsParams;
27 | public bool IsFlag { get; }
28 | public bool IsParams { get; }
29 | public bool IsHidden { get; }
30 | public bool IsDefaultValueHidden { get; }
31 | public string FormattedValueTypeName => "<" + ValueTypeName + ">";
32 |
33 | public CommandOptionHelpDefinition(string[] options, string description, string valueTypeName, string? defaultValue, int? index, bool isFlag, bool isParams, bool isHidden, bool isDefaultValueHidden)
34 | {
35 | Options = options;
36 | Description = description;
37 | ValueTypeName = valueTypeName;
38 | DefaultValue = defaultValue;
39 | Index = index;
40 | IsFlag = isFlag;
41 | IsParams = isParams;
42 | IsHidden = isHidden;
43 | IsDefaultValueHidden = isDefaultValueHidden;
44 | }
45 | }
46 |
47 | [JsonSerializable(typeof(CommandHelpDefinition))]
48 | [JsonSerializable(typeof(CommandOptionHelpDefinition))]
49 | [JsonSerializable(typeof(CommandHelpDefinition[]))]
50 | [JsonSerializable(typeof(CommandOptionHelpDefinition[]))]
51 | [JsonSerializable(typeof(string[]))]
52 | [JsonSerializable(typeof(string))]
53 | [JsonSerializable(typeof(int))]
54 | [JsonSerializable(typeof(bool))]
55 | public partial class CliSchemaJsonSerializerContext : JsonSerializerContext { }
56 |
--------------------------------------------------------------------------------
/sandbox/GeneratorSandbox/GeneratorSandbox.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net8.0
6 | preview
7 |
8 | enable
9 | disable
10 | true
11 | 1701;1702;CS8321
12 |
13 | USE_EXTERNAL_CONSOLEAPP_ABSTRACTIONS
14 |
15 | Debug;Release
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | Analyzer
40 | false
41 |
42 |
43 |
44 |
45 |
46 |
47 | PreserveNewest
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/tests/ConsoleAppFramework.GeneratorTests/ArrayParseTest.cs:
--------------------------------------------------------------------------------
1 | namespace ConsoleAppFramework.GeneratorTests;
2 |
3 | public class ArrayParseTest
4 | {
5 | VerifyHelper verifier = new VerifyHelper("CAF");
6 |
7 | [Test]
8 | public async Task Params()
9 | {
10 | var code = """
11 | ConsoleApp.Run(args, (params int[] foo) =>
12 | {
13 | Console.Write("[" + string.Join(", ", foo) + "]");
14 | });
15 | """;
16 | await verifier.Execute(code, args: "--foo", expected: "[]");
17 | await verifier.Execute(code, args: "--foo 10", expected: "[10]");
18 | await verifier.Execute(code, args: "--foo 10 20 30", expected: "[10, 20, 30]");
19 | }
20 |
21 | [Test]
22 | public async Task ArgumentParams()
23 | {
24 | var code = """
25 | ConsoleApp.Run(args, ([Argument]string title, [Argument]params int[] foo) =>
26 | {
27 | Console.Write(title + "[" + string.Join(", ", foo) + "]");
28 | });
29 | """;
30 | await verifier.Execute(code, args: "aiueo", expected: "aiueo[]");
31 | await verifier.Execute(code, args: "aiueo 10", expected: "aiueo[10]");
32 | await verifier.Execute(code, args: "aiueo 10 20 30", expected: "aiueo[10, 20, 30]");
33 | }
34 |
35 | [Test]
36 | public async Task ParseArray()
37 | {
38 | var code = """
39 | ConsoleApp.Run(args, (int[] ix, string[] sx) =>
40 | {
41 | Console.Write("[" + string.Join(", ", ix) + "]");
42 | Console.Write("[" + string.Join(", ", sx) + "]");
43 | });
44 | """;
45 | await 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]");
46 |
47 | var largeIntArray = string.Join(",", Enumerable.Range(0, 1000));
48 | var expectedIntArray = string.Join(", ", Enumerable.Range(0, 1000));
49 | await verifier.Execute(code, args: $"--ix {largeIntArray} --sx a,b,c,d,e", expected: $"[{expectedIntArray}][a, b, c, d, e]");
50 | }
51 |
52 | [Test]
53 | public async Task JsonArray()
54 | {
55 | var code = """
56 | ConsoleApp.Run(args, (int[] ix, string[] sx) =>
57 | {
58 | Console.Write("[" + string.Join(", ", ix) + "]");
59 | Console.Write("[" + string.Join(", ", sx) + "]");
60 | });
61 | """;
62 | await verifier.Execute(code, args: "--ix [] --sx []", expected: "[][]");
63 | await 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]");
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/ConsoleAppFramework/EquatableTypeSymbol.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 | using System.Collections.Immutable;
3 |
4 | namespace ConsoleAppFramework;
5 |
6 | public class EquatableTypeSymbol(ITypeSymbol typeSymbol) : IEquatable
7 | {
8 | // Used for build argument parser, maybe ok to equals name.
9 | public ITypeSymbol TypeSymbol => typeSymbol;
10 |
11 | // GetMembers is called for Enum and fields is not condition for command equality.
12 | public ImmutableArray GetMembers() => typeSymbol.GetMembers();
13 |
14 | public TypeKind TypeKind { get; } = typeSymbol.TypeKind;
15 | public SpecialType SpecialType { get; } = typeSymbol.SpecialType;
16 |
17 | public string ToFullyQualifiedFormatDisplayString() => typeSymbol.ToFullyQualifiedFormatDisplayString();
18 | public string ToDisplayString(NullableFlowState state, SymbolDisplayFormat format) => typeSymbol.ToDisplayString(state, format);
19 |
20 | public bool Equals(EquatableTypeSymbol other)
21 | {
22 | if (this.TypeKind != other.TypeKind) return false;
23 | if (this.SpecialType != other.SpecialType) return false;
24 | if (this.TypeSymbol.Name != other.TypeSymbol.Name) return false;
25 |
26 | return this.TypeSymbol.EqualsNamespaceAndName(other.TypeSymbol);
27 | }
28 | }
29 |
30 | // for filter
31 | public class EquatableTypeSymbolWithKeyedServiceKey
32 | : EquatableTypeSymbol, IEquatable
33 | {
34 | public bool IsKeyedService { get; }
35 | public string? FormattedKeyedServiceKey { get; }
36 |
37 | public EquatableTypeSymbolWithKeyedServiceKey(IParameterSymbol symbol)
38 | : base(symbol.Type)
39 | {
40 | var keyedServciesAttr = symbol.GetAttributes().FirstOrDefault(x => x.AttributeClass?.Name == "FromKeyedServicesAttribute");
41 | if (keyedServciesAttr != null)
42 | {
43 | this.IsKeyedService = true;
44 | this.FormattedKeyedServiceKey = CommandParameter.GetFormattedKeyedServiceKey(keyedServciesAttr.ConstructorArguments[0].Value);
45 | }
46 | }
47 |
48 | public bool Equals(EquatableTypeSymbolWithKeyedServiceKey other)
49 | {
50 | if (base.Equals(other))
51 | {
52 | if (IsKeyedService != other.IsKeyedService) return false;
53 | if (FormattedKeyedServiceKey != other.FormattedKeyedServiceKey) return false;
54 | return true;
55 | }
56 |
57 | return false;
58 | }
59 | }
60 |
61 | static class EquatableTypeSymbolExtensions
62 | {
63 | public static EquatableTypeSymbol ToEquatable(this ITypeSymbol typeSymbol) => new(typeSymbol);
64 | }
65 |
--------------------------------------------------------------------------------
/tests/ConsoleAppFramework.GeneratorTests/NameConverterTest.cs:
--------------------------------------------------------------------------------
1 | namespace ConsoleAppFramework.GeneratorTests;
2 |
3 | public class NameConverterTest
4 | {
5 | VerifyHelper verifier = new VerifyHelper("CAF");
6 |
7 | [Test]
8 | public async Task KebabCase()
9 | {
10 | await Assert.That(NameConverter.ToKebabCase("")).IsEqualTo("");
11 | await Assert.That(NameConverter.ToKebabCase("HelloWorld")).IsEqualTo("hello-world");
12 | await Assert.That(NameConverter.ToKebabCase("HelloWorldMyHome")).IsEqualTo("hello-world-my-home");
13 | await Assert.That(NameConverter.ToKebabCase("helloWorld")).IsEqualTo("hello-world");
14 | await Assert.That(NameConverter.ToKebabCase("hello-world")).IsEqualTo("hello-world");
15 | await Assert.That(NameConverter.ToKebabCase("A")).IsEqualTo("a");
16 | await Assert.That(NameConverter.ToKebabCase("AB")).IsEqualTo("ab");
17 | await Assert.That(NameConverter.ToKebabCase("ABC")).IsEqualTo("abc");
18 | await Assert.That(NameConverter.ToKebabCase("ABCD")).IsEqualTo("abcd");
19 | await Assert.That(NameConverter.ToKebabCase("ABCDeF")).IsEqualTo("abc-def");
20 | await Assert.That(NameConverter.ToKebabCase("XmlReader")).IsEqualTo("xml-reader");
21 | await Assert.That(NameConverter.ToKebabCase("XMLReader")).IsEqualTo("xml-reader");
22 | await Assert.That(NameConverter.ToKebabCase("MLLibrary")).IsEqualTo("ml-library");
23 | }
24 |
25 | [Test]
26 | public async Task CommandName()
27 | {
28 | await verifier.Execute("""
29 | var builder = ConsoleApp.Create();
30 | builder.Add();
31 | builder.Run(args);
32 |
33 | public class MyClass
34 | {
35 | public async Task HelloWorld()
36 | {
37 | Console.Write("Hello World!");
38 | }
39 | }
40 | """, args: "hello-world", expected: "Hello World!");
41 | }
42 |
43 | [Test]
44 | public async Task OptionName()
45 | {
46 | await verifier.Execute("""
47 | var builder = ConsoleApp.Create();
48 | builder.Add();
49 | builder.Run(args);
50 |
51 | public class MyClass
52 | {
53 | public async Task HelloWorld(string fooBar)
54 | {
55 | Console.Write("Hello World! " + fooBar);
56 | }
57 | }
58 | """, args: "hello-world --foo-bar aiueo", expected: "Hello World! aiueo");
59 |
60 |
61 | await verifier.Execute("""
62 | var builder = ConsoleApp.Create();
63 | var mc = new MyClass();
64 | builder.Add("hello-world", mc.HelloWorld);
65 | builder.Run(args);
66 |
67 | public class MyClass
68 | {
69 | public async Task HelloWorld(string fooBar)
70 | {
71 | Console.Write("Hello World! " + fooBar);
72 | }
73 | }
74 | """, args: "hello-world --foo-bar aiueo", expected: "Hello World! aiueo");
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/ConsoleAppFramework/WellKnownTypes.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 |
3 | namespace ConsoleAppFramework;
4 |
5 | public class WellKnownTypes(Compilation compilation)
6 | {
7 | INamedTypeSymbol? dateTimeOffset;
8 | public INamedTypeSymbol DateTimeOffset => dateTimeOffset ??= GetTypeByMetadataName("System.DateTimeOffset");
9 |
10 | INamedTypeSymbol? guid;
11 | public INamedTypeSymbol Guid => guid ??= GetTypeByMetadataName("System.Guid");
12 |
13 | INamedTypeSymbol? version;
14 | public INamedTypeSymbol Version => version ??= GetTypeByMetadataName("System.Version");
15 |
16 | INamedTypeSymbol? dateTime;
17 | public INamedTypeSymbol DateTime => dateTime ??= GetTypeByMetadataName("System.DateTime");
18 |
19 | INamedTypeSymbol? timeOnly;
20 | public INamedTypeSymbol TimeOnly => timeOnly ??= GetTypeByMetadataName("System.TimeOnly");
21 |
22 | INamedTypeSymbol? dateOnly;
23 | public INamedTypeSymbol DateOnly => dateOnly ??= GetTypeByMetadataName("System.DateOnly");
24 |
25 | INamedTypeSymbol? spanParsable;
26 | public INamedTypeSymbol? ISpanParsable => spanParsable ??= compilation.GetTypeByMetadataName("System.ISpanParsable`1");
27 |
28 | INamedTypeSymbol? cancellationToken;
29 | public INamedTypeSymbol CancellationToken => cancellationToken ??= GetTypeByMetadataName("System.Threading.CancellationToken");
30 |
31 | INamedTypeSymbol? task;
32 | public INamedTypeSymbol Task => task ??= GetTypeByMetadataName("System.Threading.Tasks.Task");
33 |
34 | INamedTypeSymbol? task_T;
35 | public INamedTypeSymbol Task_T => task_T ??= GetTypeByMetadataName("System.Threading.Tasks.Task`1");
36 |
37 | INamedTypeSymbol? disposable;
38 | public INamedTypeSymbol IDisposable => disposable ??= GetTypeByMetadataName("System.IDisposable");
39 |
40 | INamedTypeSymbol? asyncDisposable;
41 | public INamedTypeSymbol IAsyncDisposable => asyncDisposable ??= GetTypeByMetadataName("System.IAsyncDisposable");
42 |
43 | public bool HasTryParse(ITypeSymbol type)
44 | {
45 | if (SymbolEqualityComparer.Default.Equals(type, DateTimeOffset)
46 | || SymbolEqualityComparer.Default.Equals(type, Guid)
47 | || SymbolEqualityComparer.Default.Equals(type, DateTime)
48 | || SymbolEqualityComparer.Default.Equals(type, DateOnly)
49 | || SymbolEqualityComparer.Default.Equals(type, TimeOnly)
50 | || SymbolEqualityComparer.Default.Equals(type, Version)
51 | )
52 | {
53 | return true;
54 | }
55 | return false;
56 | }
57 |
58 | INamedTypeSymbol GetTypeByMetadataName(string metadataName)
59 | {
60 | var symbol = compilation.GetTypeByMetadataName(metadataName);
61 | if (symbol == null)
62 | {
63 | throw new InvalidOperationException($"Type {metadataName} is not found in compilation.");
64 | }
65 | return symbol;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/tests/ConsoleAppFramework.GeneratorTests/DITest.cs:
--------------------------------------------------------------------------------
1 | namespace ConsoleAppFramework.GeneratorTests;
2 |
3 | public class DITest
4 | {
5 | VerifyHelper verifier = new VerifyHelper("CAF");
6 |
7 | [Test]
8 | public async Task ServiceProvider()
9 | {
10 | await verifier.Execute("""
11 | #nullable enable
12 |
13 | var di = new MiniDI();
14 | di.Register(typeof(MyClass), new MyClass("foo"));
15 | ConsoleApp.ServiceProvider = di;
16 |
17 | ConsoleApp.Run(args, ([FromServices] MyClass mc, int x, int y) => { Console.Write(mc.Name + ":" + x + ":" + y); });
18 |
19 |
20 | class MiniDI : IServiceProvider
21 | {
22 | System.Collections.Generic.Dictionary dict = new();
23 |
24 | public void Register(Type type, object instance)
25 | {
26 | dict[type] = instance;
27 | }
28 |
29 | public object? GetService(Type serviceType)
30 | {
31 | return dict.TryGetValue(serviceType, out var instance) ? instance : null;
32 | }
33 | }
34 |
35 | class MyClass(string name)
36 | {
37 | public string Name => name;
38 | }
39 | """, args: "--x 10 --y 20", expected: "foo:10:20");
40 | }
41 |
42 | [Test]
43 | public async Task WithFilter()
44 | {
45 | await verifier.Execute("""
46 | var app = ConsoleApp.Create();
47 | app.UseFilter();
48 | app.Run(["cmd", "test"]);
49 |
50 | public class MyService
51 | {
52 | public void Test() => Console.Write("Test");
53 | }
54 |
55 | internal class MyFilter(ConsoleAppFilter next, MyService myService) : ConsoleAppFilter(next)
56 | {
57 | public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken)
58 | {
59 | myService.Test();
60 | await Next.InvokeAsync(context, cancellationToken);
61 | }
62 | }
63 |
64 | [RegisterCommands("cmd")]
65 | public class MyCommand
66 | {
67 | [Command("test")]
68 | public int Test()
69 | {
70 | return 1;
71 | }
72 | }
73 |
74 | class MiniDI : IServiceProvider
75 | {
76 | System.Collections.Generic.Dictionary dict = new();
77 |
78 | public void Register(Type type, object instance)
79 | {
80 | dict[type] = instance;
81 | }
82 |
83 | public object? GetService(Type serviceType)
84 | {
85 | return dict.TryGetValue(serviceType, out var instance) ? instance : null;
86 | }
87 | }
88 |
89 | namespace ConsoleAppFramework
90 | {
91 | partial class ConsoleApp
92 | {
93 | partial class ConsoleAppBuilder
94 | {
95 | partial void BuildAndSetServiceProvider(ConsoleAppContext context)
96 | {
97 | var di = new MiniDI();
98 | di.Register(typeof(MyService), new MyService());
99 | ConsoleApp.ServiceProvider = di;
100 | }
101 | }
102 | }
103 | }
104 | """, "cmd test", "Test");
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/ConsoleAppFramework/SourceBuilder.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 |
3 | namespace ConsoleAppFramework;
4 |
5 | // indent-level: 0. using namespace, class, struct declaration
6 | // indent-level: 1. field, property, method declaration
7 | // indent-level: 2. method body
8 | internal class SourceBuilder(int level)
9 | {
10 | StringBuilder builder = new StringBuilder();
11 |
12 | public void Indent(int levelIncr = 1)
13 | {
14 | level += levelIncr;
15 | }
16 |
17 | public void Unindent(int levelDecr = 1)
18 | {
19 | level -= levelDecr;
20 | }
21 |
22 | public Scope BeginIndent()
23 | {
24 | Indent();
25 | return new Scope(this);
26 | }
27 |
28 | public Scope BeginIndent(string code)
29 | {
30 | AppendLine(code);
31 | Indent();
32 | return new Scope(this);
33 | }
34 |
35 | public Block BeginBlock()
36 | {
37 | AppendLine("{");
38 | Indent();
39 | return new Block(this);
40 | }
41 |
42 | public Block BeginBlock(string code)
43 | {
44 | AppendLine(code);
45 | AppendLine("{");
46 | Indent();
47 | return new Block(this);
48 | }
49 |
50 | public IDisposable Nop => NullDisposable.Instance;
51 |
52 | public void AppendLine()
53 | {
54 | builder.AppendLine();
55 | }
56 |
57 | public void AppendLineIfExists(ReadOnlySpan values)
58 | {
59 | if (values.Length != 0)
60 | {
61 | builder.AppendLine();
62 | }
63 | }
64 |
65 | public void AppendLine(string text)
66 | {
67 | if (level != 0)
68 | {
69 | builder.Append(' ', level * 4); // four spaces
70 | }
71 | builder.AppendLine(text);
72 | }
73 |
74 | public void AppendWithoutIndent(string text)
75 | {
76 | builder.Append(text);
77 | }
78 |
79 | public void AppendLineWithoutIndent(string text)
80 | {
81 | builder.AppendLine(text);
82 | }
83 |
84 | public override string ToString() => builder.ToString();
85 |
86 | public struct Scope(SourceBuilder parent) : IDisposable
87 | {
88 | public void Dispose()
89 | {
90 | parent.Unindent();
91 | }
92 | }
93 |
94 | public struct Block(SourceBuilder parent) : IDisposable
95 | {
96 | public void Dispose()
97 | {
98 | parent.Unindent();
99 | parent.AppendLine("}");
100 | }
101 | }
102 |
103 | public SourceBuilder Clone()
104 | {
105 | var sb = new SourceBuilder(level);
106 | sb.builder.Append(builder.ToString());
107 | return sb;
108 | }
109 |
110 | class NullDisposable : IDisposable
111 | {
112 | public static readonly IDisposable Instance = new NullDisposable();
113 |
114 | public void Dispose()
115 | {
116 | }
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/tests/ConsoleAppFramework.NativeAotTests/NativeAotTest.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 |
3 | // This test verifies the behavior of NativeAOT.
4 | // Instead of running dotnet test, you must execute the exe generated by dotnet publish.
5 | // In the GitHub Actions workflow:
6 | // - run: dotnet publish -r linux-x64 tests/ConsoleAppFramework.NativeAotTests/ConsoleAppFramework.NativeAotTests.csproj
7 | // - run: tests/ConsoleAppFramework.NativeAotTests/bin/Release/net10.0/linux-x64/publish/ConsoleAppFramework.NativeAotTests
8 |
9 | // not parallel to check exit-code
10 | [assembly: NotInParallel]
11 |
12 | namespace ConsoleAppFramework.NativeAotTests;
13 |
14 | public class NativeAotTest
15 | {
16 | ConsoleApp.ConsoleAppBuilder app;
17 |
18 | public NativeAotTest()
19 | {
20 | Environment.ExitCode = 0; // reset ExitCode
21 |
22 | this.app = ConsoleApp.Create();
23 |
24 | app.UseFilter();
25 | app.Add("", Commands.Root);
26 | app.Add("json", Commands.RecordJson);
27 | }
28 |
29 | [Test]
30 | public async Task RunWithFilter()
31 | {
32 | // check NativeAot trimming,command requires [DynamicDependency]
33 | string[] runArgs =
34 | [
35 | "input.txt",
36 | "--count", "3",
37 | "--quiet"
38 | ];
39 |
40 | await app.RunAsync(runArgs);
41 | await Assert.That(Environment.ExitCode).IsEqualTo(0);
42 | }
43 |
44 | [Test]
45 | public async Task JsonInvalid()
46 | {
47 | string[] runArgs =
48 | [
49 | "json",
50 | "--record",
51 | "{ \"X\" = 10, \"Y\" = 20 }"
52 | ];
53 |
54 | await app.RunAsync(runArgs);
55 | await Assert.That(Environment.ExitCode).IsNotEqualTo(0);
56 | }
57 | }
58 |
59 | internal static class Commands
60 | {
61 | public static async Task Root(
62 | [Argument] string path,
63 | [Range(1, 10)] int count = 1,
64 | bool quiet = false,
65 | CancellationToken cancellationToken = default)
66 | {
67 | await Task.Delay(10, cancellationToken);
68 | if (!quiet)
69 | {
70 | Console.WriteLine($"Processing {path} with count {count}");
71 | }
72 | return 0;
73 | }
74 |
75 | public static void RecordJson(MyRecord record)
76 | {
77 | Console.WriteLine($"Record: X={record.X}, Y={record.Y}");
78 | }
79 | }
80 |
81 |
82 | internal sealed class LoggingFilter(ConsoleAppFilter next) : ConsoleAppFilter(next)
83 | {
84 | public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken)
85 | {
86 | try
87 | {
88 | await Next.InvokeAsync(context, cancellationToken);
89 | }
90 | catch (Exception ex)
91 | {
92 | Console.WriteLine($"Unhandled exception: {ex.Message}");
93 | throw;
94 | }
95 | }
96 | }
97 |
98 | public record MyRecord(int X, int Y);
99 |
--------------------------------------------------------------------------------
/tests/ConsoleAppFramework.GeneratorTests/ArgumentParserTest.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 |
3 |
4 | namespace ConsoleAppFramework.GeneratorTests;
5 |
6 | public class ArgumentParserTest
7 | {
8 | readonly VerifyHelper verifier = new("CAF");
9 |
10 | [Test]
11 | public async Task Lamda()
12 | {
13 | await verifier.Execute(HEAD + Body("""
14 | ConsoleApp.Run(args, ([Vector3Parser] Vector3 v) => Console.Write(v));
15 | """) + TAIL, args: "--v 1,2,3", expected: "<1, 2, 3>");
16 | await verifier.Execute(HEAD + Body("""
17 | var app = ConsoleApp.Create();
18 | app.Add("", ([Vector3Parser] Vector3 v) => Console.Write(v));
19 | app.Run(args);
20 | """) + TAIL, args: "--v 1,2,3", expected: "<1, 2, 3>");
21 | }
22 |
23 | [Test]
24 | public async Task Method()
25 | {
26 | await verifier.Execute(HEAD + Body("""
27 | ConsoleApp.Run(args, MyCommands.Static);
28 | """) + TAIL, args: "--v 1,2,3", expected: "<1, 2, 3>");
29 | await verifier.Execute(HEAD + Body("""
30 | var app = ConsoleApp.Create();
31 | app.Add("", MyCommands.Static);
32 | app.Run(args);
33 | """) + TAIL, args: "--v 1,2,3", expected: "<1, 2, 3>");
34 | }
35 |
36 | [Test]
37 | public async Task Class()
38 | {
39 | await verifier.Execute(HEAD + Body("""
40 | var app = ConsoleApp.Create();
41 | app.Add();
42 | app.Run(args);
43 | """) + TAIL, args: "--v 1,2,3", expected: "<1, 2, 3>");
44 | }
45 |
46 | static string Body([StringSyntax("C#-test")] string code) => code;
47 | ///
48 | ///
49 | ///
50 | [StringSyntax("C#-test")]
51 | const string
52 | HEAD = """
53 | using System.Numerics;
54 |
55 | """,
56 | TAIL = """
57 |
58 | public class MyCommands
59 | {
60 | [Command("")]
61 | public void Root([Vector3Parser] Vector3 v) => Console.Write(v);
62 | public static void Static([Vector3Parser] Vector3 v) => Console.Write(v);
63 | }
64 |
65 | [AttributeUsage(AttributeTargets.Parameter)]
66 | public class Vector3ParserAttribute : Attribute, IArgumentParser
67 | {
68 | public static bool TryParse(ReadOnlySpan s, out Vector3 result)
69 | {
70 | Span ranges = stackalloc Range[3];
71 | var splitCount = s.Split(ranges, ',');
72 | if (splitCount != 3)
73 | {
74 | result = default;
75 | return false;
76 | }
77 |
78 | float x;
79 | float y;
80 | float z;
81 | if (float.TryParse(s[ranges[0]], out x) && float.TryParse(s[ranges[1]], out y) && float.TryParse(s[ranges[2]], out z))
82 | {
83 | result = new Vector3(x, y, z);
84 | return true;
85 | }
86 |
87 | result = default;
88 | return false;
89 | }
90 | }
91 | """;
92 | }
93 |
--------------------------------------------------------------------------------
/tests/ConsoleAppFramework.GeneratorTests/BuildCustomDelegateTest.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace ConsoleAppFramework.GeneratorTests;
8 |
9 | public class BuildCustomDelegateTest
10 | {
11 | VerifyHelper verifier = new VerifyHelper("CAF");
12 |
13 | [Test]
14 | public async Task Run()
15 | {
16 | var code = """
17 | ConsoleApp.Run(args, (
18 | bool a1,
19 | bool a2,
20 | bool a3,
21 | bool a4,
22 | bool a5,
23 | bool a6,
24 | bool a7,
25 | bool a8,
26 | bool a9,
27 | bool a10,
28 | bool a11,
29 | bool a12,
30 | bool a13,
31 | bool a14,
32 | bool a15,
33 | bool a16 // ok it is Action
34 | ) => { Console.Write("ok"); });
35 | """;
36 |
37 | await verifier.Execute(code, "", "ok");
38 |
39 | var code2 = """
40 | ConsoleApp.Run(args, (
41 | bool a1,
42 | bool a2,
43 | bool a3,
44 | bool a4,
45 | bool a5,
46 | bool a6,
47 | bool a7,
48 | bool a8,
49 | bool a9,
50 | bool a10,
51 | bool a11,
52 | bool a12,
53 | bool a13,
54 | bool a14,
55 | bool a15,
56 | bool a16,
57 | bool a17 // custom delegate
58 | ) => { Console.Write("ok"); });
59 | """;
60 |
61 | await verifier.Execute(code2, "", "ok");
62 |
63 |
64 | await verifier.Execute("""
65 | var t = new Test();
66 | ConsoleApp.Run(args, t.Handle);
67 |
68 | public partial class Test
69 | {
70 | public void Handle(
71 | bool a1,
72 | bool a2,
73 | bool a3,
74 | bool a4,
75 | bool a5,
76 | bool a6,
77 | bool a7,
78 | bool a8,
79 | bool a9,
80 | bool a10,
81 | bool a11,
82 | bool a12,
83 | bool a13,
84 | bool a14,
85 | bool a15,
86 | bool a16,
87 | bool a17,
88 | bool a18,
89 | bool a19
90 | )
91 | {
92 | Console.Write("ok");
93 | }
94 | }
95 | """, "", "ok");
96 |
97 |
98 |
99 | await verifier.Execute("""
100 | unsafe
101 | {
102 | ConsoleApp.Run(args, &Test.Handle);
103 | }
104 |
105 | public partial class Test
106 | {
107 | public static void Handle(
108 | bool a1,
109 | bool a2,
110 | bool a3,
111 | bool a4,
112 | bool a5,
113 | bool a6,
114 | bool a7,
115 | bool a8,
116 | bool a9,
117 | bool a10,
118 | bool a11,
119 | bool a12,
120 | bool a13,
121 | bool a14,
122 | bool a15,
123 | bool a16,
124 | bool a17,
125 | bool a18,
126 | bool a19
127 | )
128 | {
129 | Console.Write("ok");
130 | }
131 | }
132 | """, "", "ok");
133 | }
134 |
135 | [Test]
136 | public async Task Builder()
137 | {
138 | await verifier.Execute("""
139 | var t = new Test();
140 |
141 | var app = ConsoleApp.Create();
142 | app.Add("", t.Handle);
143 | app.Run(args);
144 |
145 | public partial class Test
146 | {
147 | public void Handle(
148 | bool a1,
149 | bool a2,
150 | bool a3,
151 | bool a4,
152 | bool a5,
153 | bool a6,
154 | bool a7,
155 | bool a8,
156 | bool a9,
157 | bool a10,
158 | bool a11,
159 | bool a12,
160 | bool a13,
161 | bool a14,
162 | bool a15,
163 | bool a16,
164 | bool a17,
165 | bool a18,
166 | bool a19
167 | )
168 | {
169 | Console.Write("ok");
170 | }
171 | }
172 | """, "", "ok");
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/sandbox/GeneratorSandbox/Temp.cs:
--------------------------------------------------------------------------------
1 | ////
2 | //#nullable enable
3 | //#pragma warning disable CS0108 // hides inherited member
4 | //#pragma warning disable CS0162 // Unreachable code
5 | //#pragma warning disable CS0164 // This label has not been referenced
6 | //#pragma warning disable CS0219 // Variable assigned but never used
7 | //#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type.
8 | //#pragma warning disable CS8601 // Possible null reference assignment
9 | //#pragma warning disable CS8602
10 | //#pragma warning disable CS8604 // Possible null reference argument for parameter
11 | //#pragma warning disable CS8619
12 | //#pragma warning disable CS8620
13 | //#pragma warning disable CS8631 // The type cannot be used as type parameter in the generic type or method
14 | //#pragma warning disable CS8765 // Nullability of type of parameter
15 | //#pragma warning disable CS9074 // The 'scoped' modifier of parameter doesn't match overridden or implemented member
16 | //#pragma warning disable CA1050 // Declare types in namespaces.
17 | //#pragma warning disable CS1998
18 | //#pragma warning disable CS8625
19 |
20 | //namespace ConsoleAppFramework;
21 |
22 | //using System;
23 | //using System.Text;
24 | //using System.Reflection;
25 | //using System.Threading;
26 | //using System.Threading.Tasks;
27 | //using System.Runtime.InteropServices;
28 | //using System.Runtime.CompilerServices;
29 | //using System.Diagnostics.CodeAnalysis;
30 | //using System.ComponentModel.DataAnnotations;
31 |
32 | //using Microsoft.Extensions.DependencyInjection;
33 | //using Microsoft.Extensions.Logging;
34 | //using Microsoft.Extensions.Configuration;
35 | //using Microsoft.Extensions.Hosting;
36 |
37 | //internal static class ConsoleAppHostBuilderExtensions
38 | //{
39 | // class CompositeDisposableServiceProvider(IDisposable host, IServiceProvider serviceServiceProvider, IDisposable scope, IServiceProvider serviceProvider)
40 | // : IServiceProvider, IDisposable
41 | // {
42 | // public object? GetService(Type serviceType)
43 | // {
44 | // return serviceProvider.GetService(serviceType);
45 | // }
46 |
47 | // public void Dispose()
48 | // {
49 | // if (serviceProvider is IDisposable d)
50 | // {
51 | // d.Dispose();
52 | // }
53 | // scope.Dispose();
54 | // if (serviceServiceProvider is IDisposable d2)
55 | // {
56 | // d2.Dispose();
57 | // }
58 | // host.Dispose();
59 | // }
60 | // }
61 |
62 | // internal static ConsoleApp.ConsoleAppBuilder ToConsoleAppBuilder(this IHostBuilder hostBuilder)
63 | // {
64 | // var host = hostBuilder.Build();
65 | // var serviceServiceProvider = host.Services;
66 | // var scope = serviceServiceProvider.CreateScope();
67 | // var serviceProvider = scope.ServiceProvider;
68 | // ConsoleApp.ServiceProvider = new CompositeDisposableServiceProvider(host, serviceServiceProvider, scope, serviceProvider);
69 |
70 | // return ConsoleApp.Create();
71 | // }
72 |
73 | // internal static ConsoleApp.ConsoleAppBuilder ToConsoleAppBuilder(this HostApplicationBuilder hostBuilder)
74 | // {
75 | // var host = hostBuilder.Build();
76 | // var serviceServiceProvider = host.Services;
77 | // var scope = serviceServiceProvider.CreateScope();
78 | // var serviceProvider = scope.ServiceProvider;
79 | // ConsoleApp.ServiceProvider = new CompositeDisposableServiceProvider(host, serviceServiceProvider, scope, serviceProvider);
80 |
81 | // return ConsoleApp.Create();
82 | // }
83 | //}
84 |
--------------------------------------------------------------------------------
/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppContextTest.cs:
--------------------------------------------------------------------------------
1 | namespace ConsoleAppFramework.GeneratorTests;
2 |
3 | public class ConsoleAppContextTest
4 | {
5 | VerifyHelper verifier = new VerifyHelper("CAF");
6 |
7 | [Test]
8 | public async Task ForLambda()
9 | {
10 | await verifier.Execute("""
11 | ConsoleApp.Run(args, (ConsoleAppContext ctx) => { Console.Write(ctx.Arguments.Length); });
12 | """, args: "", expected: "0");
13 | }
14 |
15 | [Test]
16 | public async Task ForMethod()
17 | {
18 | await verifier.Execute("""
19 | var builder = ConsoleApp.Create();
20 |
21 | builder.UseFilter();
22 |
23 | builder.Add("", Hello);
24 |
25 | builder.Run(args);
26 |
27 | void Hello(ConsoleAppContext ctx)
28 | {
29 | Console.Write(ctx.State);
30 | }
31 |
32 | internal class StateFilter(ConsoleAppFilter next)
33 | : ConsoleAppFilter(next)
34 | {
35 | public override Task InvokeAsync(ConsoleAppContext context,CancellationToken cancellationToken)
36 | {
37 | Console.Write(1);
38 | return Next.InvokeAsync(context with { State = 2 }, cancellationToken);
39 | }
40 | }
41 | """, args: "", expected: "12");
42 | }
43 |
44 | [Test]
45 | [Arguments("--x 1 --y 2", "", "--x 1 --y 2", "")] // no command, no espace
46 | [Arguments("foo --x 1 --y 2", "foo", "--x 1 --y 2", "")] // command, no espace
47 | [Arguments("foo bar --x 1 --y 2", "foo bar", "--x 1 --y 2", "")] // nested command, no espace
48 | [Arguments("--x 1 --y 2 -- abc", "", "--x 1 --y 2", "abc")] // no command, espace
49 | [Arguments("--x 1 --y 2 -- abc def", "", "--x 1 --y 2", "abc def")] // no command, espace2
50 | [Arguments("foo --x 1 --y 2 -- abc", "foo", "--x 1 --y 2", "abc")] // command, espace
51 | [Arguments("foo --x 1 --y 2 -- abc def", "foo", "--x 1 --y 2", "abc def")] // command, espace2
52 | [Arguments("foo bar --x 1 --y 2 -- abc", "foo bar", "--x 1 --y 2", "abc")] // nested command, espace
53 | [Arguments("foo bar --x 1 --y 2 -- abc def", "foo bar", "--x 1 --y 2", "abc def")] // nested command, espace2
54 | public async Task ArgumentsParseTest(string args, string commandName, string expectedCommandArguments, string expectedEscapedArguments)
55 | {
56 | var argsSpan = args.Split(' ').AsSpan();
57 | var commandDepth = (commandName == "") ? 0 : (argsSpan.Length - args.Replace(commandName, "").Split(' ', StringSplitOptions.RemoveEmptyEntries).Length);
58 | var escapeIndex = argsSpan.IndexOf("--");
59 |
60 | var ctx = new ConsoleAppContext2(commandName, argsSpan.ToArray(), null, commandDepth, escapeIndex);
61 |
62 | await Assert.That(string.Join(" ", ctx.CommandArguments!)).IsEqualTo(expectedCommandArguments);
63 | await Assert.That(string.Join(" ", ctx.EscapedArguments!)).IsEqualTo(expectedEscapedArguments);
64 | }
65 |
66 | public class ConsoleAppContext2
67 | {
68 | public string CommandName { get; }
69 | public string[] Arguments { get; }
70 | public object? State { get; }
71 |
72 | int commandDepth;
73 | int escapeIndex;
74 |
75 | public ReadOnlySpan CommandArguments
76 | {
77 | get => (escapeIndex == -1)
78 | ? Arguments.AsSpan(commandDepth)
79 | : Arguments.AsSpan(commandDepth, escapeIndex - commandDepth);
80 | }
81 |
82 | public ReadOnlySpan EscapedArguments
83 | {
84 | get => (escapeIndex == -1)
85 | ? Array.Empty()
86 | : Arguments.AsSpan(escapeIndex + 1);
87 | }
88 |
89 | public ConsoleAppContext2(string commandName, string[] arguments, object? state, int commandDepth, int escapeIndex)
90 | {
91 | this.CommandName = commandName;
92 | this.Arguments = arguments;
93 | this.State = state;
94 |
95 | this.commandDepth = commandDepth;
96 | this.escapeIndex = escapeIndex;
97 | }
98 |
99 | public override string ToString()
100 | {
101 | return string.Join(" ", Arguments);
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/ConsoleAppFramework.Abstractions/ConsoleApp.Abstractions.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel;
2 |
3 | namespace ConsoleAppFramework;
4 |
5 | public interface IArgumentParser
6 | {
7 | static abstract bool TryParse(ReadOnlySpan s, out T result);
8 | }
9 |
10 | ///
11 | /// Represents the execution context for a console application command, containing command metadata and parsed arguments.
12 | ///
13 | public record ConsoleAppContext
14 | {
15 | ///
16 | /// Gets the name of the command being executed.
17 | ///
18 | public string CommandName { get; init; }
19 |
20 | ///
21 | /// Gets the raw arguments passed to the application, including the command name itself.
22 | ///
23 | public string[] Arguments { get; init; }
24 |
25 | ///
26 | /// Gets the custom state object that can be used to share data across commands.
27 | ///
28 | public object? State { get; init; }
29 |
30 | ///
31 | /// Gets the parsed global options that apply across all commands.
32 | ///
33 | public object? GlobalOptions { get; init; }
34 |
35 | ///
36 | /// Gets the depth of the command in a nested command hierarchy. Used internally by the framework.
37 | ///
38 | [EditorBrowsable(EditorBrowsableState.Never)]
39 | public int CommandDepth { get; }
40 |
41 | ///
42 | /// Gets the index of the escape separator ('--') in the arguments, or -1 if not present. Used internally by the framework.
43 | ///
44 | [EditorBrowsable(EditorBrowsableState.Never)]
45 | public int EscapeIndex { get; }
46 |
47 | ///
48 | /// Gets the internal command arguments with global options removed. Used internally by the framework.
49 | ///
50 | [EditorBrowsable(EditorBrowsableState.Never)]
51 | public ReadOnlyMemory InternalCommandArgs { get; }
52 |
53 | ///
54 | /// Gets the arguments intended for the current command, excluding the command name and any escaped arguments after '--'.
55 | ///
56 | public ReadOnlySpan CommandArguments
57 | {
58 | get => (EscapeIndex == -1)
59 | ? Arguments.AsSpan(CommandDepth)
60 | : Arguments.AsSpan(CommandDepth, EscapeIndex - CommandDepth);
61 | }
62 |
63 | ///
64 | /// Gets the arguments that appear after the escape separator ('--'), which are not parsed by the command.
65 | /// Returns an empty span if no escape separator is present.
66 | ///
67 | public ReadOnlySpan EscapedArguments
68 | {
69 | get => (EscapeIndex == -1)
70 | ? Array.Empty()
71 | : Arguments.AsSpan(EscapeIndex + 1);
72 | }
73 |
74 | public ConsoleAppContext(string commandName, string[] arguments, ReadOnlyMemory internalCommandArgs, object? state, object? globalOptions, int commandDepth, int escapeIndex)
75 | {
76 | this.CommandName = commandName;
77 | this.Arguments = arguments;
78 | this.InternalCommandArgs = internalCommandArgs;
79 | this.State = state;
80 | this.GlobalOptions = globalOptions;
81 | this.CommandDepth = commandDepth;
82 | this.EscapeIndex = escapeIndex;
83 | }
84 |
85 | ///
86 | /// Returns a string representation of all arguments joined by spaces.
87 | ///
88 | /// A space-separated string of all arguments.
89 | public override string ToString()
90 | {
91 | return string.Join(" ", Arguments);
92 | }
93 | }
94 |
95 | public abstract class ConsoleAppFilter(ConsoleAppFilter next)
96 | {
97 | protected readonly ConsoleAppFilter Next = next;
98 |
99 | public abstract Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken);
100 | }
101 |
102 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
103 | public sealed class ConsoleAppFilterAttribute : Attribute
104 | where T : ConsoleAppFilter
105 | {
106 | }
107 |
108 | public sealed class ArgumentParseFailedException(string message) : Exception(message)
109 | {
110 | }
111 |
--------------------------------------------------------------------------------
/tests/ConsoleAppFramework.GeneratorTests/HiddenAttributeTest.cs:
--------------------------------------------------------------------------------
1 | namespace ConsoleAppFramework.GeneratorTests;
2 |
3 | public class HiddenAtttributeTest
4 | {
5 | VerifyHelper verifier = new("CAF");
6 |
7 | [Test]
8 | public async Task VerifyHiddenOptions_Lambda()
9 | {
10 | var code =
11 | """
12 | ConsoleApp.Log = x => Console.WriteLine(x);
13 | ConsoleApp.Run(args, (int x, [Hidden]int y) => { });
14 | """;
15 |
16 | // Verify Hidden options is not shown on command help.
17 | await verifier.Execute(code, args: "--help", expected:
18 | """
19 | Usage: [options...] [-h|--help] [--version]
20 |
21 | Options:
22 | --x [Required]
23 |
24 | """);
25 | }
26 |
27 | [Test]
28 | public async Task VerifyHiddenCommands_Class()
29 | {
30 | var code =
31 | """
32 | ConsoleApp.Log = x => Console.WriteLine(x);
33 | var builder = ConsoleApp.Create();
34 | builder.Add();
35 | await builder.RunAsync(args);
36 |
37 | public class Commands
38 | {
39 | [Hidden]
40 | public async Task Command1() { Console.Write("command1"); }
41 |
42 | public async Task Command2() { Console.Write("command2"); }
43 |
44 | [Hidden]
45 | public async Task Command3(int x, [Hidden]int y) { Console.Write($"command3: x={x} y={y}"); }
46 | }
47 | """;
48 |
49 | // Verify hidden command is not shown on root help commands.
50 | await verifier.Execute(code, args: "--help", expected:
51 | """
52 | Usage: [command] [-h|--help] [--version]
53 |
54 | Commands:
55 | command2
56 |
57 | """);
58 |
59 | // Verify Hidden command help is shown when explicitly specify command name.
60 | await verifier.Execute(code, args: "command1 --help", expected:
61 | """
62 | Usage: command1 [-h|--help] [--version]
63 |
64 | """);
65 |
66 | await verifier.Execute(code, args: "command2 --help", expected:
67 | """
68 | Usage: command2 [-h|--help] [--version]
69 |
70 | """);
71 |
72 | await verifier.Execute(code, args: "command3 --help", expected:
73 | """
74 | Usage: command3 [options...] [-h|--help] [--version]
75 |
76 | Options:
77 | --x [Required]
78 |
79 | """);
80 |
81 | // Verify commands involations
82 | await verifier.Execute(code, args: "command1", "command1");
83 | await verifier.Execute(code, args: "command2", "command2");
84 | await verifier.Execute(code, args: "command3 --x 1 --y 2", expected: "command3: x=1 y=2");
85 | }
86 |
87 | [Test]
88 | public async Task VerifyHiddenCommands_LocalFunctions()
89 | {
90 | var code =
91 | """
92 | ConsoleApp.Log = x => Console.WriteLine(x);
93 | var builder = ConsoleApp.Create();
94 |
95 | builder.Add("", () => { Console.Write("root"); });
96 | builder.Add("command1", Command1);
97 | builder.Add("command2", Command2);
98 | builder.Add("command3", Command3);
99 | builder.Run(args);
100 |
101 | [Hidden]
102 | static void Command1() { Console.Write("command1"); }
103 |
104 | static void Command2() { Console.Write("command2"); }
105 |
106 | [Hidden]
107 | static void Command3(int x, [Hidden]int y) { Console.Write($"command3: x={x} y={y}"); }
108 | """;
109 |
110 | await verifier.Execute(code, args: "--help", expected:
111 | """
112 | Usage: [command] [-h|--help] [--version]
113 |
114 | Commands:
115 | command2
116 |
117 | """);
118 |
119 | // Verify commands can be invoked.
120 | await verifier.Execute(code, args: "command1", expected: "command1");
121 | await verifier.Execute(code, args: "command2", expected: "command2");
122 | await verifier.Execute(code, args: "command3 --x 1 --y 2", expected: "command3: x=1 y=2");
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/sandbox/GeneratorSandbox/Filters.cs:
--------------------------------------------------------------------------------
1 |
2 | using ConsoleAppFramework;
3 | using System.ComponentModel.DataAnnotations;
4 |
5 | // using Microsoft.Extensions.DependencyInjection;
6 | using System.Diagnostics;
7 | using System.Reflection;
8 |
9 | namespace GeneratorSandbox;
10 |
11 | // ReadMe sample filters
12 |
13 | internal class NopFilter(ConsoleAppFilter next) : ConsoleAppFilter(next)
14 | {
15 | public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken)
16 | {
17 | try
18 | {
19 | /* on before */
20 | await Next.InvokeAsync(context, cancellationToken); // next
21 | /* on after */
22 | }
23 | catch
24 | {
25 | /* on error */
26 | throw;
27 | }
28 | finally
29 | {
30 | /* on finally */
31 | }
32 | }
33 | }
34 |
35 |
36 |
37 | internal class AuthenticationFilter(ConsoleAppFilter next) : ConsoleAppFilter(next)
38 | {
39 | public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken)
40 | {
41 | var requestId = Guid.NewGuid();
42 | var userId = await GetUserIdAsync();
43 |
44 | // setup new state to context
45 | var authedContext = context with { State = new ApplicationContext(requestId, userId) };
46 | await Next.InvokeAsync(authedContext, cancellationToken);
47 | }
48 |
49 | // get user-id from DB/auth saas/others
50 | async Task GetUserIdAsync()
51 | {
52 | await Task.Delay(TimeSpan.FromSeconds(1));
53 | return 1999;
54 | }
55 | }
56 |
57 | record class ApplicationContext(Guid RequestId, int UserId);
58 |
59 | internal class LogRunningTimeFilter(ConsoleAppFilter next) : ConsoleAppFilter(next)
60 | {
61 | public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken)
62 | {
63 | var startTime = Stopwatch.GetTimestamp();
64 | ConsoleApp.Log($"Execute command at {DateTime.UtcNow.ToLocalTime()}"); // LocalTime for human readable time
65 | try
66 | {
67 | await Next.InvokeAsync(context, cancellationToken);
68 | ConsoleApp.Log($"Command execute successfully at {DateTime.UtcNow.ToLocalTime()}, Elapsed: " + (Stopwatch.GetElapsedTime(startTime)));
69 | }
70 | catch
71 | {
72 | ConsoleApp.Log($"Command execute failed at {DateTime.UtcNow.ToLocalTime()}, Elapsed: " + (Stopwatch.GetElapsedTime(startTime)));
73 | throw;
74 | }
75 | }
76 | }
77 |
78 | internal class ChangeExitCodeFilter(ConsoleAppFilter next) : ConsoleAppFilter(next)
79 | {
80 | public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken)
81 | {
82 | try
83 | {
84 | await Next.InvokeAsync(context, cancellationToken);
85 | }
86 | catch (Exception ex)
87 | {
88 | if (ex is OperationCanceledException) return;
89 |
90 | Environment.ExitCode = 9999; // change custom exit code
91 | ConsoleApp.LogError(ex.ToString());
92 | }
93 | }
94 | }
95 |
96 | internal class PreventMultipleSameCommandInvokeFilter(ConsoleAppFilter next) : ConsoleAppFilter(next)
97 | {
98 | public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken)
99 | {
100 | var basePath = Assembly.GetEntryAssembly()?.Location.Replace(Path.DirectorySeparatorChar, '_');
101 | var mutexKey = $"{basePath}$$${context.CommandName}"; // lock per command-name
102 |
103 | using var mutex = new Mutex(true, mutexKey, out var createdNew);
104 | if (!createdNew)
105 | {
106 | throw new Exception($"already running command:{context.CommandName} in another process.");
107 | }
108 |
109 | await Next.InvokeAsync(context, cancellationToken);
110 | }
111 | }
112 |
113 |
114 | //internal class ServiceProviderScopeFilter(IServiceProvider serviceProvider, ConsoleAppFilter next) : ConsoleAppFilter(next)
115 | //{
116 | // public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken)
117 | // {
118 | // // create Microsoft.Extensions.DependencyInjection scope
119 | // await using var scope = serviceProvider.CreateAsyncScope();
120 | // await Next.InvokeAsync(context, cancellationToken);
121 | // }
122 | //}
123 |
--------------------------------------------------------------------------------
/sandbox/CliFrameworkBenchmark/Benchmark.cs:
--------------------------------------------------------------------------------
1 | // This benchmark project is based on CliFx.Benchmarks.
2 | // https://github.com/Tyrrrz/CliFx/tree/master/CliFx.Benchmarks/
3 |
4 | using BenchmarkDotNet.Attributes;
5 | using BenchmarkDotNet.Order;
6 | using CliFx;
7 | using CliFrameworkBenchmarks.Commands;
8 | using ConsoleAppFramework;
9 | using Spectre.Console.Cli;
10 |
11 | namespace CliFrameworkBenchmarks;
12 |
13 | [Orderer(SummaryOrderPolicy.FastestToSlowest)]
14 | public class Benchmark
15 | {
16 | private static readonly string[] Arguments = { "--str", "hello world", "-i", "13", "-b" };
17 |
18 | [Benchmark(Description = "Cocona.Lite")]
19 | public void ExecuteWithCoconaLite()
20 | {
21 | Cocona.CoconaLiteApp.Run(Arguments);
22 | }
23 |
24 | [Benchmark(Description = "Cocona")]
25 | public void ExecuteWithCocona()
26 | {
27 | Cocona.CoconaApp.Run(Arguments);
28 | }
29 |
30 | //[Benchmark(Description = "ConsoleAppFramework")]
31 | //public async ValueTask ExecuteWithConsoleAppFramework() =>
32 | // await Host.CreateDefaultBuilder().RunConsoleAppFrameworkAsync(Arguments);
33 |
34 | [Benchmark(Description = "CliFx")]
35 | public ValueTask ExecuteWithCliFx()
36 | {
37 | return new CliApplicationBuilder().AddCommand(typeof(CliFxCommand)).Build().RunAsync(Arguments);
38 | }
39 |
40 | [Benchmark(Description = "System.CommandLine v2")]
41 | public int ExecuteWithSystemCommandLine()
42 | {
43 | return SystemCommandLineCommand.ParseInvoke(Arguments);
44 | }
45 |
46 | [Benchmark(Description = "System.CommandLine v2(InvokeAsync)")]
47 | public Task ExecuteWithSystemCommandLineAsync()
48 | {
49 | return SystemCommandLineCommand.ParseInvokeAsync(Arguments);
50 | }
51 |
52 | //[Benchmark(Description = "McMaster.Extensions.CommandLineUtils")]
53 | //public int ExecuteWithMcMaster() =>
54 | // McMaster.Extensions.CommandLineUtils.CommandLineApplication.Execute(Arguments);
55 |
56 | //[Benchmark(Description = "CommandLineParser")]
57 | //public void ExecuteWithCommandLineParser() =>
58 | // new Parser()
59 | // .ParseArguments(Arguments, typeof(CommandLineParserCommand))
60 | // .WithParsed(c => c.Execute());
61 |
62 | //[Benchmark(Description = "PowerArgs")]
63 | //public void ExecuteWithPowerArgs() =>
64 | // PowerArgs.Args.InvokeMain(Arguments);
65 |
66 | //[Benchmark(Description = "Clipr")]
67 | //public void ExecuteWithClipr() =>
68 | // clipr.CliParser.Parse(Arguments).Execute();
69 |
70 |
71 | //[Benchmark(Description = "ConsoleAppFramework v5")]
72 | //public void ExecuteConsoleAppFramework5()
73 | //{
74 | // ConsoleApp.Run(Arguments, ConsoleAppFrameworkCommand.Execute);
75 | //}
76 |
77 | [Benchmark(Description = "ConsoleAppFramework v5", Baseline = true)]
78 | public void ExecuteConsoleAppFramework()
79 | {
80 | ConsoleApp.Run(Arguments, ConsoleAppFrameworkCommand.Execute);
81 | }
82 |
83 | [Benchmark(Description = "ConsoleAppFramework v5(app with CancellationToken)")]
84 | public Task ExecuteConsoleAppFramework2()
85 | {
86 | var app = ConsoleApp.Create();
87 | app.Add("", ConsoleAppFrameworkCommand.ExecuteWithCancellationToken);
88 | return app.RunAsync(Arguments);
89 | }
90 |
91 | // for alpha testing
92 | //private static readonly string[] TempArguments = { "", "--str", "hello world", "-i", "13", "-b" };
93 | //[Benchmark(Description = "ConsoleAppFramework.Builder")]
94 | //public unsafe void ExecuteConsoleAppFrameworkBuilder()
95 | //{
96 | // var builder = ConsoleApp.Create();
97 | // builder.Add("", ConsoleAppFrameworkCommand.Execute);
98 | // builder.Run(TempArguments);
99 | //}
100 |
101 | [Benchmark(Description = "Spectre.Console.Cli")]
102 | public void ExecuteSpectreConsoleCli()
103 | {
104 | var app = new CommandApp();
105 | app.Run(Arguments);
106 | }
107 |
108 |
109 | //[Benchmark(Description = "ConsoleAppFramework Builder API")]
110 | //public unsafe void ExecuteConsoleAppFramework2()
111 | //{
112 | // var app = ConsoleApp.Create();
113 | // app.Add("", ConsoleAppFrameworkCommand.Execute);
114 | // app.Run(Arguments);
115 | //}
116 |
117 | //[Benchmark(Description = "ConsoleAppFramework CancellationToken")]
118 | //public unsafe void ExecuteConsoleAppFramework3()
119 | //{
120 | // var app = ConsoleApp.Create();
121 | // app.Add("", ConsoleAppFrameworkCommandWithCancellationToken.Execute);
122 | // app.Run(Arguments);
123 | //}
124 |
125 | //[Benchmark(Description = "ConsoleAppFramework With Filter")]
126 | //public unsafe void ExecuteConsoleAppFramework4()
127 | //{
128 | // var app = ConsoleApp.Create();
129 | // app.UseFilter();
130 | // app.Add("", ConsoleAppFrameworkCommand.Execute);
131 | // app.Run(Arguments);
132 | //}
133 | }
134 |
--------------------------------------------------------------------------------
/tests/ConsoleAppFramework.GeneratorTests/SubCommandTest.cs:
--------------------------------------------------------------------------------
1 | namespace ConsoleAppFramework.GeneratorTests;
2 |
3 | public class SubCommandTest
4 | {
5 | VerifyHelper verifier = new VerifyHelper("CAF");
6 |
7 | [Test]
8 | public async Task Zeroargs()
9 | {
10 | var code = """
11 | var builder = ConsoleApp.Create();
12 |
13 | builder.Add("", () => { Console.Write("root"); });
14 | builder.Add("a", () => { Console.Write("a"); });
15 | builder.Add("a b1", () => { Console.Write("a b1"); });
16 | builder.Add("a b2", () => { Console.Write("a b2"); });
17 | builder.Add("a b2 c", () => { Console.Write("a b2 c"); });
18 | builder.Add("a b2 d", () => { Console.Write("a b2 d"); });
19 | builder.Add("a b2 d e", () => { Console.Write("a b2 d e"); });
20 | builder.Add("a b c d e f", () => { Console.Write("a b c d e f"); });
21 |
22 | builder.Run(args);
23 | """;
24 |
25 | await verifier.Execute(code, "", "root");
26 | await verifier.Execute(code, "a", "a");
27 | await verifier.Execute(code, "a b1", "a b1");
28 | await verifier.Execute(code, "a b2", "a b2");
29 | await verifier.Execute(code, "a b2 c", "a b2 c");
30 | await verifier.Execute(code, "a b2 d", "a b2 d");
31 | await verifier.Execute(code, "a b2 d e", "a b2 d e");
32 | await verifier.Execute(code, "a b c d e f", "a b c d e f");
33 | }
34 |
35 | [Test]
36 | public async Task Withargs()
37 | {
38 | var code = """
39 | var builder = ConsoleApp.Create();
40 |
41 | builder.Add("", (int x, int y) => { Console.Write($"root {x} {y}"); });
42 | builder.Add("a", (int x, int y) => { Console.Write($"a {x} {y}"); });
43 | builder.Add("a b1", (int x, int y) => { Console.Write($"a b1 {x} {y}"); });
44 | builder.Add("a b2", (int x, int y) => { Console.Write($"a b2 {x} {y}"); });
45 | builder.Add("a b2 c", (int x, int y) => { Console.Write($"a b2 c {x} {y}"); });
46 | builder.Add("a b2 d", (int x, int y) => { Console.Write($"a b2 d {x} {y}"); });
47 | builder.Add("a b2 d e", (int x, int y) => { Console.Write($"a b2 d e {x} {y}"); });
48 | builder.Add("a b c d e f", (int x, int y) => { Console.Write($"a b c d e f {x} {y}"); });
49 |
50 | builder.Run(args);
51 | """;
52 |
53 | await verifier.Execute(code, "--x 10 --y 20", "root 10 20");
54 | await verifier.Execute(code, "a --x 10 --y 20", "a 10 20");
55 | await verifier.Execute(code, "a b1 --x 10 --y 20", "a b1 10 20");
56 | await verifier.Execute(code, "a b2 --x 10 --y 20", "a b2 10 20");
57 | await verifier.Execute(code, "a b2 c --x 10 --y 20", "a b2 c 10 20");
58 | await verifier.Execute(code, "a b2 d --x 10 --y 20", "a b2 d 10 20");
59 | await verifier.Execute(code, "a b2 d e --x 10 --y 20", "a b2 d e 10 20");
60 | await verifier.Execute(code, "a b c d e f --x 10 --y 20", "a b c d e f 10 20");
61 | }
62 |
63 | [Test]
64 | public async Task ZeroargsAsync()
65 | {
66 | var code = """
67 | var builder = ConsoleApp.Create();
68 |
69 | builder.Add("", () => { Console.Write("root"); });
70 | builder.Add("a", () => { Console.Write("a"); });
71 | builder.Add("a b1", () => { Console.Write("a b1"); });
72 | builder.Add("a b2", () => { Console.Write("a b2"); });
73 | builder.Add("a b2 c", () => { Console.Write("a b2 c"); });
74 | builder.Add("a b2 d", () => { Console.Write("a b2 d"); });
75 | builder.Add("a b2 d e", () => { Console.Write("a b2 d e"); });
76 | builder.Add("a b c d e f", () => { Console.Write("a b c d e f"); });
77 |
78 | await builder.RunAsync(args);
79 | """;
80 |
81 | await verifier.Execute(code, "", "root");
82 | await verifier.Execute(code, "a", "a");
83 | await verifier.Execute(code, "a b1", "a b1");
84 | await verifier.Execute(code, "a b2", "a b2");
85 | await verifier.Execute(code, "a b2 c", "a b2 c");
86 | await verifier.Execute(code, "a b2 d", "a b2 d");
87 | await verifier.Execute(code, "a b2 d e", "a b2 d e");
88 | await verifier.Execute(code, "a b c d e f", "a b c d e f");
89 | }
90 |
91 | [Test]
92 | public async Task WithargsAsync()
93 | {
94 | var code = """
95 | var builder = ConsoleApp.Create();
96 |
97 | builder.Add("", (int x, int y) => { Console.Write($"root {x} {y}"); });
98 | builder.Add("a", (int x, int y) => { Console.Write($"a {x} {y}"); });
99 | builder.Add("a b1", (int x, int y) => { Console.Write($"a b1 {x} {y}"); });
100 | builder.Add("a b2", (int x, int y) => { Console.Write($"a b2 {x} {y}"); });
101 | builder.Add("a b2 c", (int x, int y) => { Console.Write($"a b2 c {x} {y}"); });
102 | builder.Add("a b2 d", (int x, int y) => { Console.Write($"a b2 d {x} {y}"); });
103 | builder.Add("a b2 d e", (int x, int y) => { Console.Write($"a b2 d e {x} {y}"); });
104 | builder.Add("a b c d e f", (int x, int y) => { Console.Write($"a b c d e f {x} {y}"); });
105 |
106 | await builder.RunAsync(args);
107 | """;
108 |
109 | await verifier.Execute(code, "--x 10 --y 20", "root 10 20");
110 | await verifier.Execute(code, "a --x 10 --y 20", "a 10 20");
111 | await verifier.Execute(code, "a b1 --x 10 --y 20", "a b1 10 20");
112 | await verifier.Execute(code, "a b2 --x 10 --y 20", "a b2 10 20");
113 | await verifier.Execute(code, "a b2 c --x 10 --y 20", "a b2 c 10 20");
114 | await verifier.Execute(code, "a b2 d --x 10 --y 20", "a b2 d 10 20");
115 | await verifier.Execute(code, "a b2 d e --x 10 --y 20", "a b2 d e 10 20");
116 | await verifier.Execute(code, "a b c d e f --x 10 --y 20", "a b c d e f 10 20");
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/ConsoleAppFramework/DiagnosticDescriptors.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 |
3 | namespace ConsoleAppFramework;
4 |
5 | internal sealed class DiagnosticReporter
6 | {
7 | List? diagnostics;
8 |
9 | public bool HasDiagnostics => diagnostics != null && diagnostics.Count != 0;
10 |
11 | public void ReportDiagnostic(DiagnosticDescriptor diagnosticDescriptor, Location location, params object?[]? messageArgs)
12 | {
13 | var diagnostic = Diagnostic.Create(diagnosticDescriptor, location, messageArgs);
14 | if (diagnostics == null)
15 | {
16 | diagnostics = new();
17 | }
18 | diagnostics.Add(diagnostic);
19 | }
20 |
21 | public void ReportToContext(SourceProductionContext context)
22 | {
23 | if (diagnostics != null)
24 | {
25 | foreach (var item in diagnostics)
26 | {
27 | context.ReportDiagnostic(item);
28 | }
29 | }
30 | }
31 | }
32 |
33 | internal static class DiagnosticDescriptors
34 | {
35 | const string Category = "GenerateConsoleAppFramework";
36 |
37 | public static void ReportDiagnostic(this SourceProductionContext context, DiagnosticDescriptor diagnosticDescriptor, Location location, params object?[]? messageArgs)
38 | {
39 | var diagnostic = Diagnostic.Create(diagnosticDescriptor, location, messageArgs);
40 | context.ReportDiagnostic(diagnostic);
41 | }
42 |
43 | public static DiagnosticDescriptor Create(int id, string message)
44 | {
45 | return Create(id, message, message);
46 | }
47 |
48 | public static DiagnosticDescriptor Create(int id, string title, string messageFormat)
49 | {
50 | return new DiagnosticDescriptor(
51 | id: "CAF" + id.ToString("000"),
52 | title: title,
53 | messageFormat: messageFormat,
54 | category: Category,
55 | defaultSeverity: DiagnosticSeverity.Error,
56 | isEnabledByDefault: true);
57 | }
58 |
59 | public static DiagnosticDescriptor RequireArgsAndMethod { get; } = Create(
60 | 1,
61 | "ConsoleApp.Run/RunAsync requires string[] args and lambda/method in arguments.");
62 |
63 | public static DiagnosticDescriptor ReturnTypeLambda { get; } = Create(
64 | 2,
65 | "Command lambda expressions return type must be void or int or async Task or async Task.",
66 | "Command lambda expressions return type must be void or int or async Task or async Task but returned '{0}'.");
67 |
68 | public static DiagnosticDescriptor ReturnTypeMethod { get; } = Create(
69 | 3,
70 | "Command method return type must be void or int or async Task or async Task.",
71 | "Command method return type must be void or int or async Task or async Task but returned '{0}'.");
72 |
73 | // v5.7.7 supports non-first argument parameters
74 | //public static DiagnosticDescriptor SequentialArgument { get; } = Create(
75 | // 4,
76 | // "All Argument parameters must be sequential from first.");
77 |
78 | public static DiagnosticDescriptor FunctionPointerCanNotHaveValidation { get; } = Create(
79 | 5,
80 | "Function pointer can not have validation.");
81 |
82 | public static DiagnosticDescriptor AddCommandMustBeStringLiteral { get; } = Create(
83 | 6,
84 | "ConsoleAppBuilder.Add string command must be string literal.");
85 |
86 | public static DiagnosticDescriptor DuplicateCommandName { get; } = Create(
87 | 7,
88 | "Command name is duplicated.",
89 | "Command name '{0}' is duplicated.");
90 |
91 | public static DiagnosticDescriptor AddInLoopIsNotAllowed { get; } = Create(
92 | 8,
93 | "ConsoleAppBuilder.Add/UseFilter is not allowed in loop statements(while, do, for, foreach).");
94 |
95 | public static DiagnosticDescriptor CommandHasFilter { get; } = Create(
96 | 9,
97 | "ConsoleApp.Run does not allow the use of filters, but the function has a filter attribute.");
98 |
99 | public static DiagnosticDescriptor FilterMultipleConstructor { get; } = Create(
100 | 10,
101 | "ConsoleAppFilter class does not allow multiple constructors.");
102 |
103 | public static DiagnosticDescriptor ClassMultipleConstructor { get; } = Create(
104 | 11,
105 | "ConsoleAppBuilder.Add class does not allow multiple constructors.");
106 |
107 | public static DiagnosticDescriptor ClassHasNoPublicMethods { get; } = Create(
108 | 12,
109 | "ConsoleAppBuilder.Add class must have at least one public method.");
110 |
111 | public static DiagnosticDescriptor ClassIsStaticOrAbstract { get; } = Create(
112 | 13,
113 | "ConsoleAppBuilder.Add class does not allow static or abstract classes.");
114 |
115 | public static DiagnosticDescriptor DefinedInOtherProject { get; } = Create(
116 | 14,
117 | "ConsoleAppFramework cannot register type/method in another project outside the SourceGenerator referenced project.");
118 |
119 | public static DiagnosticDescriptor DocCommentParameterNameNotMatched { get; } = Create(
120 | 15,
121 | "Document Comment parameter name '{0}' does not match method parameter name.");
122 |
123 | public static DiagnosticDescriptor ReturnTypeMethodAsyncVoid { get; } = Create(
124 | 16,
125 | "Command method return type does not allow async void.");
126 |
127 | public static DiagnosticDescriptor DuplicateConfigureGlobalOptions { get; } = Create(
128 | 17,
129 | "ConfigureGlobalOptions does not allow to invoke twice.");
130 |
131 | public static DiagnosticDescriptor InvalidGlobalOptionsType { get; } = Create(
132 | 18,
133 | "GlobalOption parameter type only allows compile-time constant(primitives, string, enum) and there nullable.");
134 | }
135 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | # User-specific files
5 | *.suo
6 | *.user
7 | *.userosscache
8 | *.sln.docstates
9 |
10 | # User-specific files (MonoDevelop/Xamarin Studio)
11 | *.userprefs
12 |
13 | # Build results
14 | [Dd]ebug/
15 | [Dd]ebugPublic/
16 | [Rr]elease/
17 | [Rr]eleases/
18 | x64/
19 | x86/
20 | bld/
21 | [Bb]in/
22 | [Oo]bj/
23 | [Ll]og/
24 |
25 | # Visual Studio 2015 cache/options directory
26 | .vs/
27 | # Uncomment if you have tasks that create the project's static files in wwwroot
28 | #wwwroot/
29 |
30 | # MSTest test Results
31 | [Tt]est[Rr]esult*/
32 | [Bb]uild[Ll]og.*
33 |
34 | # NUNIT
35 | *.VisualState.xml
36 | TestResult.xml
37 |
38 | # Build Results of an ATL Project
39 | [Dd]ebugPS/
40 | [Rr]eleasePS/
41 | dlldata.c
42 |
43 | # DNX
44 | project.lock.json
45 | project.fragment.lock.json
46 | artifacts/
47 |
48 | *_i.c
49 | *_p.c
50 | *_i.h
51 | *.ilk
52 | *.meta
53 | *.obj
54 | *.pch
55 | *.pdb
56 | *.pgc
57 | *.pgd
58 | *.sbr
59 | *.tlb
60 | *.tli
61 | *.tlh
62 | *.tmp
63 | *.tmp_proj
64 | *.log
65 | *.vspscc
66 | *.vssscc
67 | .builds
68 | *.pidb
69 | *.svclog
70 | *.scc
71 |
72 | # Chutzpah Test files
73 | _Chutzpah*
74 |
75 | # Visual C++ cache files
76 | ipch/
77 | *.aps
78 | *.ncb
79 | *.opendb
80 | *.opensdf
81 | *.sdf
82 | *.cachefile
83 | *.VC.db
84 | *.VC.VC.opendb
85 |
86 | # Visual Studio profiler
87 | *.psess
88 | *.vsp
89 | *.vspx
90 | *.sap
91 |
92 | # TFS 2012 Local Workspace
93 | $tf/
94 |
95 | # Guidance Automation Toolkit
96 | *.gpState
97 |
98 | # ReSharper is a .NET coding add-in
99 | _ReSharper*/
100 | *.[Rr]e[Ss]harper
101 | *.DotSettings.user
102 |
103 | # JustCode is a .NET coding add-in
104 | .JustCode
105 |
106 | # TeamCity is a build add-in
107 | _TeamCity*
108 |
109 | # DotCover is a Code Coverage Tool
110 | *.dotCover
111 |
112 | # NCrunch
113 | _NCrunch_*
114 | .*crunch*.local.xml
115 | nCrunchTemp_*
116 |
117 | # MightyMoose
118 | *.mm.*
119 | AutoTest.Net/
120 |
121 | # Web workbench (sass)
122 | .sass-cache/
123 |
124 | # Installshield output folder
125 | [Ee]xpress/
126 |
127 | # DocProject is a documentation generator add-in
128 | DocProject/buildhelp/
129 | DocProject/Help/*.HxT
130 | DocProject/Help/*.HxC
131 | DocProject/Help/*.hhc
132 | DocProject/Help/*.hhk
133 | DocProject/Help/*.hhp
134 | DocProject/Help/Html2
135 | DocProject/Help/html
136 |
137 | # Click-Once directory
138 | publish/
139 |
140 | # Publish Web Output
141 | *.[Pp]ublish.xml
142 | *.azurePubxml
143 | *.publishproj
144 |
145 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
146 | # checkin your Azure Web App publish settings, but sensitive information contained
147 | # in these scripts will be unencrypted
148 | PublishScripts/
149 |
150 | # NuGet Packages
151 | *.nupkg
152 | # The packages folder can be ignored because of Package Restore
153 | **/packages/*
154 | # except build/, which is used as an MSBuild target.
155 | !**/packages/build/
156 | # Uncomment if necessary however generally it will be regenerated when needed
157 | #!**/packages/repositories.config
158 | # NuGet v3's project.json files produces more ignoreable files
159 | *.nuget.props
160 | *.nuget.targets
161 |
162 | # Microsoft Azure Build Output
163 | csx/
164 | *.build.csdef
165 |
166 | # Microsoft Azure Emulator
167 | ecf/
168 | rcf/
169 |
170 | # Windows Store app package directories and files
171 | AppPackages/
172 | BundleArtifacts/
173 | Package.StoreAssociation.xml
174 | _pkginfo.txt
175 |
176 | # Visual Studio cache files
177 | # files ending in .cache can be ignored
178 | *.[Cc]ache
179 | # but keep track of directories ending in .cache
180 | !*.[Cc]ache/
181 |
182 | # Others
183 | ClientBin/
184 | ~$*
185 | *~
186 | *.dbmdl
187 | *.dbproj.schemaview
188 | *.jfm
189 | *.pfx
190 | *.publishsettings
191 | node_modules/
192 | orleans.codegen.cs
193 |
194 | # Since there are multiple workflows, uncomment next line to ignore bower_components
195 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
196 | #bower_components/
197 |
198 | # RIA/Silverlight projects
199 | Generated_Code/
200 |
201 | # Backup & report files from converting an old project file
202 | # to a newer Visual Studio version. Backup files are not needed,
203 | # because we have git ;-)
204 | _UpgradeReport_Files/
205 | Backup*/
206 | UpgradeLog*.XML
207 | UpgradeLog*.htm
208 |
209 | # SQL Server files
210 | *.mdf
211 | *.ldf
212 |
213 | # Business Intelligence projects
214 | *.rdl.data
215 | *.bim.layout
216 | *.bim_*.settings
217 |
218 | # Microsoft Fakes
219 | FakesAssemblies/
220 |
221 | # GhostDoc plugin setting file
222 | *.GhostDoc.xml
223 |
224 | # Node.js Tools for Visual Studio
225 | .ntvs_analysis.dat
226 |
227 | # Visual Studio 6 build log
228 | *.plg
229 |
230 | # Visual Studio 6 workspace options file
231 | *.opt
232 |
233 | # Visual Studio LightSwitch build output
234 | **/*.HTMLClient/GeneratedArtifacts
235 | **/*.DesktopClient/GeneratedArtifacts
236 | **/*.DesktopClient/ModelManifest.xml
237 | **/*.Server/GeneratedArtifacts
238 | **/*.Server/ModelManifest.xml
239 | _Pvt_Extensions
240 |
241 | # Paket dependency manager
242 | .paket/paket.exe
243 | paket-files/
244 |
245 | # FAKE - F# Make
246 | .fake/
247 |
248 | # JetBrains Rider
249 | .idea/
250 | *.sln.iml
251 |
252 | # CodeRush
253 | .cr/
254 |
255 | # Python Tools for Visual Studio (PTVS)
256 | __pycache__/
257 | *.pyc
258 |
259 | # Unity
260 |
261 | src/MessagePack.UnityClient/bin/*
262 | src/MessagePack.UnityClient/Library/*
263 | src/MessagePack.UnityClient/obj/*
264 | src/MessagePack.UnityClient/Temp/*
265 |
266 | # Project Specified
267 |
268 | nuget/mpc.exe
269 | nuget/mpc.exe.config
270 |
271 | nuget/tools/*
272 | nuget/unity/tools/*
273 | nuget/unity*
274 | /nuget/*.unitypackage
275 |
276 | # VSCode
277 | .vscode/*
278 | !.vscode/settings.json
279 | !.vscode/tasks.json
280 | !.vscode/launch.json
281 | !.vscode/extensions.json
282 | sandbox/**/Properties/launchSettings.json
283 | circleci.exe
284 |
--------------------------------------------------------------------------------
/tests/ConsoleAppFramework.GeneratorTests/FilterTest.cs:
--------------------------------------------------------------------------------
1 | namespace ConsoleAppFramework.GeneratorTests;
2 |
3 | public class FilterTest
4 | {
5 | VerifyHelper verifier = new VerifyHelper("CAF");
6 |
7 | [Test]
8 | public async Task ForLambda()
9 | {
10 | await verifier.Execute("""
11 | var builder = ConsoleApp.Create();
12 |
13 | builder.UseFilter();
14 | builder.UseFilter();
15 |
16 | builder.Add("", Hello);
17 |
18 | builder.Run(args);
19 |
20 | [ConsoleAppFilter]
21 | [ConsoleAppFilter]
22 | void Hello()
23 | {
24 | Console.Write("abcde");
25 | }
26 |
27 | internal class NopFilter1(ConsoleAppFilter next)
28 | : ConsoleAppFilter(next)
29 | {
30 | public override Task InvokeAsync(ConsoleAppContext context,CancellationToken cancellationToken)
31 | {
32 | Console.Write(1);
33 | return Next.InvokeAsync(context, cancellationToken);
34 | }
35 | }
36 |
37 | internal class NopFilter2(ConsoleAppFilter next)
38 | : ConsoleAppFilter(next)
39 | {
40 | public override Task InvokeAsync(ConsoleAppContext context,CancellationToken cancellationToken)
41 | {
42 | Console.Write(2);
43 | return Next.InvokeAsync(context, cancellationToken);
44 | }
45 | }
46 |
47 | internal class NopFilter3(ConsoleAppFilter next)
48 | : ConsoleAppFilter(next)
49 | {
50 | public override Task InvokeAsync(ConsoleAppContext context,CancellationToken cancellationToken)
51 | {
52 | Console.Write(3);
53 | return Next.InvokeAsync(context, cancellationToken);
54 | }
55 | }
56 |
57 | internal class NopFilter4(ConsoleAppFilter next)
58 | : ConsoleAppFilter(next)
59 | {
60 | public override Task InvokeAsync(ConsoleAppContext context,CancellationToken cancellationToken)
61 | {
62 | Console.Write(4);
63 | return Next.InvokeAsync(context, cancellationToken);
64 | }
65 | }
66 | """, args: "", expected: "1234abcde");
67 | }
68 |
69 | [Test]
70 | public async Task ForClass()
71 | {
72 | await verifier.Execute("""
73 | var builder = ConsoleApp.Create();
74 |
75 | builder.UseFilter();
76 | builder.UseFilter();
77 |
78 | builder.Add();
79 |
80 | await builder.RunAsync(args);
81 |
82 | [ConsoleAppFilter]
83 | [ConsoleAppFilter]
84 | public class MyClass
85 | {
86 | [ConsoleAppFilter]
87 | [ConsoleAppFilter]
88 | public void Hello()
89 | {
90 | Console.Write("abcde");
91 | }
92 | }
93 |
94 | internal class NopFilter1(ConsoleAppFilter next)
95 | : ConsoleAppFilter(next)
96 | {
97 | public override Task InvokeAsync(ConsoleAppContext context,CancellationToken cancellationToken)
98 | {
99 | Console.Write(1);
100 | return Next.InvokeAsync(context, cancellationToken);
101 | }
102 | }
103 |
104 | internal class NopFilter2(ConsoleAppFilter next)
105 | : ConsoleAppFilter(next)
106 | {
107 | public override Task InvokeAsync(ConsoleAppContext context,CancellationToken cancellationToken)
108 | {
109 | Console.Write(2);
110 | return Next.InvokeAsync(context, cancellationToken);
111 | }
112 | }
113 |
114 | internal class NopFilter3(ConsoleAppFilter next)
115 | : ConsoleAppFilter(next)
116 | {
117 | public override Task InvokeAsync(ConsoleAppContext context,CancellationToken cancellationToken)
118 | {
119 | Console.Write(3);
120 | return Next.InvokeAsync(context, cancellationToken);
121 | }
122 | }
123 |
124 | internal class NopFilter4(ConsoleAppFilter next)
125 | : ConsoleAppFilter(next)
126 | {
127 | public override Task InvokeAsync(ConsoleAppContext context,CancellationToken cancellationToken)
128 | {
129 | Console.Write(4);
130 | return Next.InvokeAsync(context, cancellationToken);
131 | }
132 | }
133 |
134 | internal class NopFilter5(ConsoleAppFilter next)
135 | : ConsoleAppFilter(next)
136 | {
137 | public override Task InvokeAsync(ConsoleAppContext context,CancellationToken cancellationToken)
138 | {
139 | Console.Write(5);
140 | return Next.InvokeAsync(context, cancellationToken);
141 | }
142 | }
143 |
144 | internal class NopFilter6(ConsoleAppFilter next)
145 | : ConsoleAppFilter(next)
146 | {
147 | public override Task InvokeAsync(ConsoleAppContext context,CancellationToken cancellationToken)
148 | {
149 | Console.Write(6);
150 | return Next.InvokeAsync(context, cancellationToken);
151 | }
152 | }
153 | """, args: "hello", expected: "123456abcde");
154 | }
155 |
156 |
157 | [Test]
158 | public async Task DI()
159 | {
160 | await verifier.Execute("""
161 | var serviceCollection = new MiniDI();
162 | serviceCollection.Register(typeof(string), "hoge!");
163 | serviceCollection.Register(typeof(int), 9999);
164 | ConsoleApp.ServiceProvider = serviceCollection;
165 |
166 | var builder = ConsoleApp.Create();
167 |
168 | builder.UseFilter();
169 |
170 | builder.Add("", () => Console.Write("do"));
171 |
172 | builder.Run(args);
173 |
174 | internal class DIFilter(string foo, int bar, ConsoleAppFilter next)
175 | : ConsoleAppFilter(next)
176 | {
177 | public override Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken)
178 | {
179 | Console.Write("invoke:");
180 | Console.Write(foo);
181 | Console.Write(bar);
182 | return Next.InvokeAsync(context, cancellationToken);
183 | }
184 | }
185 |
186 | public class MiniDI : IServiceProvider
187 | {
188 | System.Collections.Generic.Dictionary dict = new();
189 |
190 | public void Register(Type type, object instance)
191 | {
192 | dict[type] = instance;
193 | }
194 |
195 | public object? GetService(Type serviceType)
196 | {
197 | return dict.TryGetValue(serviceType, out var instance) ? instance : null;
198 | }
199 | }
200 | """, args: "", expected: "invoke:hoge!9999do");
201 | }
202 | }
203 |
--------------------------------------------------------------------------------
/tests/ConsoleAppFramework.GeneratorTests/GlobalOptionTest.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace ConsoleAppFramework.GeneratorTests;
8 |
9 | public class GlobalOptionTest
10 | {
11 | VerifyHelper verifier = new VerifyHelper("CAF");
12 |
13 | [Test]
14 | public async Task BooleanParseCheck()
15 | {
16 | string BuildCode(string parameter)
17 | {
18 | return $$"""
19 | var app = ConsoleApp.Create();
20 | app.ConfigureGlobalOptions((ref ConsoleApp.GlobalOptionsBuilder builder) => builder.AddGlobalOption("{{parameter}}", ""));
21 | app.Add("", (ConsoleAppContext context) => Console.Write(context.GlobalOptions));
22 | app.Run(args);
23 | """;
24 | }
25 |
26 | await verifier.Execute(BuildCode("-v"), "-v", "True");
27 | await verifier.Execute(BuildCode("-no"), "-v", "False");
28 | await verifier.Execute(BuildCode("-v|--verbose"), "-v", "True");
29 | await verifier.Execute(BuildCode("-v|--verbose"), "--verbose", "True");
30 | await verifier.Execute(BuildCode("-v|--verbose|--vo-v"), "-v", "True");
31 | await verifier.Execute(BuildCode("-v|--verbose|--vo-v"), "--verbose", "True");
32 | await verifier.Execute(BuildCode("-v|--verbose|--vo-v"), "--vo-v", "True");
33 | await verifier.Execute(BuildCode("-v|--verbose|--vo-v"), "--no", "False");
34 | }
35 |
36 | [Test]
37 | public async Task ArgumentRemove()
38 | {
39 | var code = $$"""
40 | var app = ConsoleApp.Create();
41 | app.ConfigureGlobalOptions((ref ConsoleApp.GlobalOptionsBuilder builder) =>
42 | {
43 | var p = builder.AddGlobalOption("--parameter", "", 0);
44 | return p;
45 | });
46 | app.Add("", (int x, int y, ConsoleAppContext context) =>
47 | {
48 | Console.Write($"{context.GlobalOptions} -> ({x}, {y})");
49 | });
50 | app.Run(args);
51 | """;
52 |
53 | // first
54 | await verifier.Execute(code, "--parameter 100 --x 10 --y 20", "100 -> (10, 20)");
55 |
56 | // middle
57 | await verifier.Execute(code, "--x 10 --parameter 100 --y 20", "100 -> (10, 20)");
58 |
59 | // last
60 | await verifier.Execute(code, "--x 10 --y 20 --parameter 100", "100 -> (10, 20)");
61 | }
62 |
63 | [Test]
64 | public async Task EnumParse()
65 | {
66 | await verifier.Execute("""
67 | var app = ConsoleApp.Create();
68 | app.ConfigureGlobalOptions((ref ConsoleApp.GlobalOptionsBuilder builder) =>
69 | {
70 | var p = builder.AddGlobalOption("--parameter", "", 0);
71 | var d = builder.AddGlobalOption("--dry-run", "");
72 | var f = builder.AddGlobalOption("--fruit", "", Fruit.Orange);
73 | return (p, d, f);
74 | });
75 | app.Add("", (int x, int y, ConsoleAppContext context) =>
76 | {
77 | Console.Write($"{context.GlobalOptions} -> {(x, y)}");
78 | });
79 | app.Run(args);
80 |
81 | enum Fruit
82 | {
83 | Orange, Apple, Grape
84 | }
85 | """, "--parameter 100 --x 10 --dry-run --y 20 --fruit grape", "(100, True, Grape) -> (10, 20)");
86 | }
87 |
88 | [Test]
89 | public async Task EnumErrorShowsValidValues()
90 | {
91 | var result = verifier.Error("""
92 | ConsoleApp.Log = x => Console.Write(x);
93 |
94 | var app = ConsoleApp.Create();
95 | app.ConfigureGlobalOptions((ref ConsoleApp.GlobalOptionsBuilder builder) =>
96 | {
97 | var fruit = builder.AddGlobalOption("--fruit", "", Fruit.Apple);
98 | return fruit;
99 | });
100 |
101 | app.Add("", (ConsoleAppContext context) => { });
102 | app.Run(args);
103 |
104 | enum Fruit
105 | {
106 | Orange, Apple, Grape
107 | }
108 | """, "--fruit Potato");
109 |
110 | await Assert.That(result.Stdout).Contains("Argument '--fruit' is invalid. Provided value: Potato. Valid values: Orange, Apple, Grape");
111 | await Assert.That(result.ExitCode).IsEqualTo(1);
112 | }
113 |
114 | [Test]
115 | public async Task DefaultValueForOption()
116 | {
117 | await verifier.Execute("""
118 | var app = ConsoleApp.Create();
119 | app.ConfigureGlobalOptions((ref ConsoleApp.GlobalOptionsBuilder builder) =>
120 | {
121 | var p = builder.AddGlobalOption("--parameter", "", -10);
122 | var d = builder.AddGlobalOption("--dry-run", "");
123 | var f = builder.AddGlobalOption("--fruit", "", Fruit.Apple);
124 | return (p, d, f);
125 | });
126 | app.Add("", (int x, int y, ConsoleAppContext context) =>
127 | {
128 | Console.Write($"{context.GlobalOptions} -> {(x, y)}");
129 | });
130 | app.Run(args);
131 |
132 | enum Fruit
133 | {
134 | Orange, Apple, Grape
135 | }
136 |
137 | """, "--x 10 --y 20", "(-10, False, Apple) -> (10, 20)");
138 | }
139 |
140 | [Test]
141 | public async Task RequiredParse()
142 | {
143 | var error = verifier.Error("""
144 | var app = ConsoleApp.Create();
145 | app.ConfigureGlobalOptions((ref ConsoleApp.GlobalOptionsBuilder builder) =>
146 | {
147 | var p = builder.AddRequiredGlobalOption("--parameter", "");
148 | var d = builder.AddGlobalOption("--dry-run", "");
149 | return (p, d);
150 | });
151 | app.Add("", (int x, int y, ConsoleAppContext context) =>
152 | {
153 | Console.Write($"{context.GlobalOptions} -> {(x, y)}");
154 | });
155 | app.Run(args);
156 | """, "--x 10 --dry-run --y 20");
157 |
158 | error.Stdout.Contains("Required argument '--parameter' was not specified.");
159 | }
160 |
161 | [Test]
162 | public async Task NamedParameter()
163 | {
164 | await verifier.Execute("""
165 | var app = ConsoleApp.Create();
166 | app.ConfigureGlobalOptions((ref ConsoleApp.GlobalOptionsBuilder builder) =>
167 | {
168 | var p = builder.AddGlobalOption("--parameter", defaultValue: 1000);
169 | var d = builder.AddGlobalOption(description: "foo", name: "--dry-run");
170 | return (p, d);
171 | });
172 | app.Add("", (int x, int y, ConsoleAppContext context) =>
173 | {
174 | Console.Write($"{context.GlobalOptions} -> {(x, y)}");
175 | });
176 | app.Run(args);
177 | """, "--x 10 --dry-run --y 20", "(1000, True) -> (10, 20)");
178 | }
179 |
180 | [Test]
181 | public async Task DoubleDashEscape()
182 | {
183 | await verifier.Execute("""
184 | var app = ConsoleApp.Create();
185 |
186 | app.ConfigureGlobalOptions((ref ConsoleApp.GlobalOptionsBuilder builder) =>
187 | {
188 | var flag = builder.AddGlobalOption("--global-flag");
189 | return new GlobalOptions(flag);
190 | });
191 |
192 | app.UseFilter();
193 | app.Add();
194 | app.Run(args);
195 |
196 | internal record GlobalOptions(string Flag);
197 |
198 | internal class Commands
199 | {
200 | [Command("some-command")]
201 | public async Task SomeCommand([Argument] string commandArg, ConsoleAppContext context)
202 | {
203 | Console.WriteLine($"ARG: {commandArg}");
204 | Console.WriteLine($"ESCAPED: {string.Join(", ", context.EscapedArguments.ToArray()!)}");
205 | }
206 | }
207 |
208 | internal class SomeFilter(ConsoleAppFilter next) : ConsoleAppFilter(next)
209 | {
210 | public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken)
211 | {
212 | Console.WriteLine($"FLAG: {((GlobalOptions)context.GlobalOptions!).Flag}");
213 | await Next.InvokeAsync(context, cancellationToken);
214 | }
215 | }
216 | """, "some-command hello --global-flag flag-value -- more args here", """
217 | FLAG: flag-value
218 | ARG: hello
219 | ESCAPED: more, args, here
220 |
221 | """);
222 | }
223 | }
224 |
--------------------------------------------------------------------------------
/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppBuilderTest.cs:
--------------------------------------------------------------------------------
1 | namespace ConsoleAppFramework.GeneratorTests;
2 |
3 | public class ConsoleAppBuilderTest
4 | {
5 | VerifyHelper verifier = new VerifyHelper("CAF");
6 |
7 | [Test]
8 | public async Task BuilderRun()
9 | {
10 | var code = """
11 | var builder = ConsoleApp.Create();
12 | builder.Add("foo", (int x, int y) => { Console.Write(x + y); });
13 | builder.Add("bar", (int x, int y = 10) => { Console.Write(x + y); });
14 | builder.Add("baz", int (int x, string y) => { Console.Write(x + y); return 10; });
15 | builder.Add("boz", async Task (int x) => { await Task.Yield(); Console.Write(x * 2); });
16 | builder.Run(args);
17 | """;
18 | await verifier.Execute(code, "foo --x 10 --y 20", "30");
19 | await verifier.Execute(code, "bar --x 20 --y 30", "50");
20 | var exitCode = await verifier.Execute(code, "bar --x 20", "30");
21 | await Assert.That(exitCode).IsZero();
22 | exitCode = await verifier.Execute(code, "baz --x 40 --y takoyaki", "40takoyaki");
23 | await Assert.That(exitCode).IsEqualTo(10);
24 |
25 | await verifier.Execute(code, "boz --x 40", "80");
26 | }
27 |
28 | [Test]
29 | public async Task BuilderRunAsync()
30 | {
31 | var code = """
32 | var builder = ConsoleApp.Create();
33 | builder.Add("foo", (int x, int y) => { Console.Write(x + y); });
34 | builder.Add("bar", (int x, int y = 10) => { Console.Write(x + y); });
35 | builder.Add("baz", int (int x, string y) => { Console.Write(x + y); return 10; });
36 | builder.Add("boz", async Task (int x) => { await Task.Yield(); Console.Write(x * 2); });
37 | await builder.RunAsync(args);
38 | """;
39 |
40 | await verifier.Execute(code, "foo --x 10 --y 20", "30");
41 | await verifier.Execute(code, "bar --x 20 --y 30", "50");
42 | var exitCode = await verifier.Execute(code, "bar --x 20", "30");
43 | await Assert.That(exitCode).IsZero();
44 | exitCode = await verifier.Execute(code, "baz --x 40 --y takoyaki", "40takoyaki");
45 | await Assert.That(exitCode).IsEqualTo(10);
46 |
47 | await verifier.Execute(code, "boz --x 40", "80");
48 | }
49 |
50 | [Test]
51 | public async Task AddClass()
52 | {
53 | var code = """
54 | var builder = ConsoleApp.Create();
55 | builder.Add();
56 | await builder.RunAsync(args);
57 |
58 | public class MyClass
59 | {
60 | public void Do()
61 | {
62 | Console.Write("yeah");
63 | }
64 |
65 | public void Sum(int x, int y)
66 | {
67 | Console.Write(x + y);
68 | }
69 |
70 | public void Echo(string msg)
71 | {
72 | Console.Write(msg);
73 | }
74 |
75 | void Echo()
76 | {
77 | }
78 |
79 | public static void Sum()
80 | {
81 | }
82 | }
83 | """;
84 |
85 | await verifier.Execute(code, "do", "yeah");
86 | await verifier.Execute(code, "sum --x 1 --y 2", "3");
87 | await verifier.Execute(code, "echo --msg takoyaki", "takoyaki");
88 | }
89 |
90 | [Test]
91 | public async Task ClassDispose()
92 | {
93 | await verifier.Execute("""
94 | var builder = ConsoleApp.Create();
95 | builder.Add();
96 | builder.Run(args);
97 |
98 | public class MyClass : IDisposable
99 | {
100 | public void Do()
101 | {
102 | Console.Write("yeah:");
103 | }
104 |
105 | public void Dispose()
106 | {
107 | Console.Write("disposed!");
108 | }
109 | }
110 | """, "do", "yeah:disposed!");
111 |
112 | await verifier.Execute("""
113 | var builder = ConsoleApp.Create();
114 | builder.Add();
115 | await builder.RunAsync(args);
116 |
117 | public class MyClass : IDisposable
118 | {
119 | public void Do()
120 | {
121 | Console.Write("yeah:");
122 | }
123 |
124 | public void Dispose()
125 | {
126 | Console.Write("disposed!");
127 | }
128 | }
129 | """, "do", "yeah:disposed!");
130 |
131 | await verifier.Execute("""
132 | var builder = ConsoleApp.Create();
133 | builder.Add();
134 | await builder.RunAsync(args);
135 |
136 | public class MyClass : IAsyncDisposable
137 | {
138 | public void Do()
139 | {
140 | Console.Write("yeah:");
141 | }
142 |
143 | public ValueTask DisposeAsync()
144 | {
145 | Console.Write("disposed!");
146 | return default;
147 | }
148 | }
149 | """, "do", "yeah:disposed!");
150 |
151 | // DisposeAsync: sync pattern
152 | await verifier.Execute("""
153 | var builder = ConsoleApp.Create();
154 | builder.Add();
155 | builder.Run(args);
156 |
157 | public class MyClass : IAsyncDisposable
158 | {
159 | public void Do()
160 | {
161 | Console.Write("yeah:");
162 | }
163 |
164 | public ValueTask DisposeAsync()
165 | {
166 | Console.Write("disposed!");
167 | return default;
168 | }
169 | }
170 | """, "do", "yeah:disposed!");
171 | }
172 |
173 | [Test]
174 | public async Task ClassWithDI()
175 | {
176 | await verifier.Execute("""
177 | var serviceCollection = new MiniDI();
178 | serviceCollection.Register(typeof(string), "hoge!");
179 | serviceCollection.Register(typeof(int), 9999);
180 | ConsoleApp.ServiceProvider = serviceCollection;
181 |
182 | var builder = ConsoleApp.Create();
183 | builder.Add();
184 | builder.Run(args);
185 |
186 | public class MyClass(string foo, int bar)
187 | {
188 | public void Do()
189 | {
190 | Console.Write("yeah:");
191 | Console.Write(foo);
192 | Console.Write(bar);
193 | }
194 | }
195 |
196 | public class MiniDI : IServiceProvider
197 | {
198 | System.Collections.Generic.Dictionary dict = new();
199 |
200 | public void Register(Type type, object instance)
201 | {
202 | dict[type] = instance;
203 | }
204 |
205 | public object GetService(Type serviceType)
206 | {
207 | return dict.TryGetValue(serviceType, out var instance) ? instance : null;
208 | }
209 | }
210 | """, "do", "yeah:hoge!9999");
211 | }
212 |
213 | [Test]
214 | public async Task CommandAttr()
215 | {
216 | var code = """
217 | var builder = ConsoleApp.Create();
218 | builder.Add();
219 | builder.Run(args);
220 |
221 | public class MyClass()
222 | {
223 | [Command("nomunomu")]
224 | public void Do()
225 | {
226 | Console.Write("yeah");
227 | }
228 | }
229 | """;
230 |
231 | await verifier.Execute(code, "nomunomu", "yeah");
232 | }
233 |
234 | [Test]
235 | public async Task CommandAttrWithFilter()
236 | {
237 | var code = """
238 | var builder = ConsoleApp.Create();
239 | builder.Add();
240 | builder.Run(args);
241 |
242 | public class MyClass()
243 | {
244 | [ConsoleAppFilter]
245 | [Command("nomunomu")]
246 | [ConsoleAppFilter]
247 | public void Do()
248 | {
249 | Console.Write("command");
250 | }
251 | }
252 |
253 | internal class NopFilter1(ConsoleAppFilter next)
254 | : ConsoleAppFilter(next)
255 | {
256 | public override Task InvokeAsync(ConsoleAppContext context,CancellationToken cancellationToken)
257 | {
258 | Console.Write("filter1-");
259 | return Next.InvokeAsync(context, cancellationToken);
260 | }
261 | }
262 | internal class NopFilter2(ConsoleAppFilter next)
263 | : ConsoleAppFilter(next)
264 | {
265 | public override Task InvokeAsync(ConsoleAppContext context,CancellationToken cancellationToken)
266 | {
267 | Console.Write("filter2-");
268 | return Next.InvokeAsync(context, cancellationToken);
269 | }
270 | }
271 | """;
272 |
273 | await verifier.Execute(code, "nomunomu", "filter1-filter2-command");
274 | }
275 |
276 | [Test]
277 | public async Task CommandAlias()
278 | {
279 | var code = """
280 | var app = ConsoleApp.Create();
281 |
282 | app.Add("build|b", () => { Console.Write("build ok"); });
283 | app.Add("test|t", () => { Console.Write("test ok"); });
284 | app.Add();
285 |
286 | app.Run(args);
287 |
288 | public class Commands
289 | {
290 | /// Analyze the current package and report errors, but don't build object files.
291 | [Command("check|c")]
292 | public void Check() { Console.Write("check ok"); }
293 |
294 | /// Build this packages's and its dependencies' documenation.
295 | [Command("doc|d")]
296 | public void Doc() { Console.Write("doc ok"); }
297 | }
298 | """;
299 |
300 | await verifier.Execute(code, "b", "build ok");
301 | await verifier.Execute(code, "build", "build ok");
302 | await verifier.Execute(code, "t", "test ok");
303 | await verifier.Execute(code, "test", "test ok");
304 | await verifier.Execute(code, "c", "check ok");
305 | await verifier.Execute(code, "check", "check ok");
306 | await verifier.Execute(code, "d", "doc ok");
307 | await verifier.Execute(code, "doc", "doc ok");
308 | }
309 | }
310 |
--------------------------------------------------------------------------------
/src/ConsoleAppFramework/RoslynExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 | using Microsoft.CodeAnalysis.CSharp.Syntax;
3 | using Microsoft.CodeAnalysis.CSharp;
4 |
5 | namespace ConsoleAppFramework;
6 |
7 | internal static class RoslynExtensions
8 | {
9 | internal static string ToFullyQualifiedFormatDisplayString(this ITypeSymbol typeSymbol)
10 | {
11 | return typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
12 | }
13 |
14 | public static bool EqualsUnconstructedGenericType(this INamedTypeSymbol left, INamedTypeSymbol right)
15 | {
16 | var l = left.IsGenericType ? left.ConstructUnboundGenericType() : left;
17 | var r = right.IsGenericType ? right.ConstructUnboundGenericType() : right;
18 | return SymbolEqualityComparer.Default.Equals(l, r);
19 | }
20 |
21 | public static IEnumerable GetBaseTypes(this INamedTypeSymbol type, bool includeSelf = false)
22 | {
23 | if (includeSelf) yield return type;
24 | var baseType = type.BaseType;
25 | while (baseType != null)
26 | {
27 | yield return baseType;
28 | baseType = baseType.BaseType;
29 | }
30 | }
31 |
32 | public static bool EqualsNamespaceAndName(this ITypeSymbol? left, ITypeSymbol? right)
33 | {
34 | if (left == null && right == null) return true;
35 | if (left == null || right == null) return false;
36 |
37 | var l = left.ContainingNamespace;
38 | var r = right.ContainingNamespace;
39 | while (l != null && r != null)
40 | {
41 | if (l.Name != r.Name) return false;
42 |
43 | l = l.ContainingNamespace;
44 | r = r.ContainingNamespace;
45 | }
46 |
47 | return (left.Name == right.Name);
48 | }
49 |
50 | public static bool ZipEquals(this IEnumerable left, IEnumerable right, Func predicate)
51 | where T : IMethodSymbol
52 | {
53 | using var e1 = left.GetEnumerator();
54 | using var e2 = right.GetEnumerator();
55 | while (true)
56 | {
57 | var b1 = e1.MoveNext();
58 | var b2 = e2.MoveNext();
59 |
60 | if (b1 != b2) return false; // different sequence length, ng
61 | if (b1 == false) return true; // both false, ok
62 |
63 | if (!predicate(e1.Current, e2.Current))
64 | {
65 | return false;
66 | }
67 | }
68 | }
69 |
70 | public static ParameterListSyntax? GetParameterListOfConstructor(this SyntaxNode node)
71 | {
72 | if (node is ConstructorDeclarationSyntax ctor)
73 | {
74 | return ctor.ParameterList;
75 | }
76 | else if (node is ClassDeclarationSyntax primaryCtor)
77 | {
78 | return primaryCtor.ParameterList;
79 | }
80 | else
81 | {
82 | return null;
83 | }
84 | }
85 |
86 | public static Location Clone(this Location location)
87 | {
88 | // without inner SyntaxTree
89 | return Location.Create(location.SourceTree?.FilePath ?? "", location.SourceSpan, location.GetLineSpan().Span);
90 | }
91 |
92 | public static DocumentationCommentTriviaSyntax? GetDocumentationCommentTriviaSyntax(this SyntaxNode node)
93 | {
94 | // Hack note:
95 | // ISymbol.GetDocumentationCommentXml requirestrue>.
96 | // However, getting the DocumentationCommentTrivia of a SyntaxNode also requires the same condition.
97 | // It can only be obtained when DocumentationMode is Parse or Diagnostic, but whenfalse>,
98 | // it becomes None, and the necessary Trivia cannot be obtained.
99 | // Therefore, we will attempt to reparse and retrieve it.
100 |
101 | // About DocumentationMode and Trivia: https://github.com/dotnet/roslyn/issues/58210
102 | if (node.SyntaxTree.Options.DocumentationMode == DocumentationMode.None)
103 | {
104 | var withDocumentationComment = node.SyntaxTree.Options.WithDocumentationMode(DocumentationMode.Parse);
105 | var code = node.ToFullString();
106 | var newTree = CSharpSyntaxTree.ParseText(code, (CSharpParseOptions)withDocumentationComment);
107 | node = newTree.GetRoot();
108 | }
109 |
110 | foreach (var leadingTrivia in node.GetLeadingTrivia())
111 | {
112 | if (leadingTrivia.GetStructure() is DocumentationCommentTriviaSyntax structure)
113 | {
114 | return structure;
115 | }
116 | }
117 |
118 | return null;
119 | }
120 |
121 | static IEnumerable GetXmlElements(this SyntaxList content, string elementName)
122 | {
123 | foreach (XmlNodeSyntax syntax in content)
124 | {
125 | if (syntax is XmlEmptyElementSyntax emptyElement)
126 | {
127 | if (string.Equals(elementName, emptyElement.Name.ToString(), StringComparison.Ordinal))
128 | {
129 | yield return emptyElement;
130 | }
131 |
132 | continue;
133 | }
134 |
135 | if (syntax is XmlElementSyntax elementSyntax)
136 | {
137 | if (string.Equals(elementName, elementSyntax.StartTag?.Name?.ToString(), StringComparison.Ordinal))
138 | {
139 | yield return elementSyntax;
140 | }
141 |
142 | continue;
143 | }
144 | }
145 | }
146 |
147 | public static string GetSummary(this DocumentationCommentTriviaSyntax docComment)
148 | {
149 | var summary = docComment.Content.GetXmlElements("summary").FirstOrDefault() as XmlElementSyntax;
150 | if (summary == null) return "";
151 |
152 | return summary.Content.ToString().Replace("///", "").Trim();
153 | }
154 |
155 | public static IEnumerable<(string Name, string Description)> GetParams(this DocumentationCommentTriviaSyntax docComment)
156 | {
157 | foreach (var item in docComment.Content.GetXmlElements("param").OfType())
158 | {
159 | var name = item.StartTag.Attributes.OfType().FirstOrDefault()?.Identifier.Identifier.ValueText.Replace("///", "").Trim() ?? "";
160 | var desc = item.Content.ToString().Replace("///", "").Trim() ?? "";
161 | yield return (name, desc);
162 | }
163 |
164 | yield break;
165 | }
166 |
167 | public static void GetConstantValues(this ArgumentListSyntax argumentListSyntax, SemanticModel model,
168 | string name1, string name2,
169 | ref object? value1, ref object? value2)
170 | {
171 | var arguments = argumentListSyntax.Arguments;
172 | for (int i = 0; i < arguments.Count; i++)
173 | {
174 | var arg = arguments[i];
175 | var constant = model.GetConstantValue(arg.Expression);
176 | if (constant.HasValue)
177 | {
178 | var constantValue = constant.Value;
179 | if (arg.NameColon != null)
180 | {
181 | var name = arg.NameColon.Name.Identifier.Text;
182 | if (name == name1)
183 | {
184 | value1 = constantValue;
185 | }
186 | else if (name == name2)
187 | {
188 | value2 = constantValue;
189 | }
190 | }
191 | else
192 | {
193 | if (i == 0) value1 = constantValue;
194 | else if (i == 1) value2 = constantValue;
195 | }
196 | }
197 | }
198 | }
199 |
200 | public static void GetConstantValues(this ArgumentListSyntax argumentListSyntax, SemanticModel model,
201 | string name1, string name2, string name3,
202 | ref object? value1, ref object? value2, ref object? value3)
203 | {
204 | var arguments = argumentListSyntax.Arguments;
205 | for (int i = 0; i < arguments.Count; i++)
206 | {
207 | var arg = arguments[i];
208 | var constant = model.GetConstantValue(arg.Expression);
209 | if (constant.HasValue)
210 | {
211 | var constantValue = constant.Value;
212 | if (arg.NameColon != null)
213 | {
214 | var name = arg.NameColon.Name.Identifier.Text;
215 | if (name == name1)
216 | {
217 | value1 = constantValue;
218 | }
219 | else if (name == name2)
220 | {
221 | value2 = constantValue;
222 | }
223 | else if (name == name3)
224 | {
225 | value3 = constantValue;
226 | }
227 | }
228 | else
229 | {
230 | if (i == 0) value1 = constantValue;
231 | else if (i == 1) value2 = constantValue;
232 | else if (i == 2) value3 = constantValue;
233 | }
234 | }
235 | }
236 | }
237 |
238 | }
239 |
--------------------------------------------------------------------------------
/tests/ConsoleAppFramework.GeneratorTests/RunTest.cs:
--------------------------------------------------------------------------------
1 | namespace ConsoleAppFramework.GeneratorTests;
2 |
3 | public class Test
4 | {
5 | VerifyHelper verifier = new VerifyHelper("CAF");
6 |
7 | [Test]
8 | public async Task SyncRun()
9 | {
10 | await verifier.Execute("ConsoleApp.Run(args, (int x, int y) => { Console.Write((x + y)); });", "--x 10 --y 20", "30");
11 | }
12 |
13 | [Test]
14 | public async Task OptionTokenShouldNotFillArgumentSlot()
15 | {
16 | var code = """
17 | ConsoleApp.Log = x => Console.Write(x);
18 | ConsoleApp.Run(args, ([Argument] string path, bool dryRun) =>
19 | {
20 | Console.Write((dryRun, path).ToString());
21 | });
22 | """;
23 |
24 | await Assert.That(verifier.Error(code, "--dry-run").Stdout).Contains("Required argument 'path' was not specified.");
25 | await verifier.Execute(code, "--dry-run sample.txt", "(True, sample.txt)");
26 | }
27 |
28 | [Test]
29 | public async Task OptionTokenAllowsMultipleArguments()
30 | {
31 | var code = """
32 | ConsoleApp.Log = x => Console.Write(x);
33 | ConsoleApp.Run(args, ([Argument] string source, [Argument] string destination, bool dryRun) =>
34 | {
35 | Console.Write((dryRun, source, destination).ToString());
36 | });
37 | """;
38 |
39 | await verifier.Execute(code, "--dry-run input.json output.json", "(True, input.json, output.json)");
40 | }
41 |
42 | [Test]
43 | public async Task OptionTokenRespectsArgumentDefaultValue()
44 | {
45 | var code = """
46 | ConsoleApp.Run(args, ([Argument] string path = "default-path", bool dryRun = false) =>
47 | {
48 | Console.Write((dryRun, path).ToString());
49 | });
50 | """;
51 |
52 | await verifier.Execute(code, "--dry-run", "(True, default-path)");
53 | }
54 |
55 | [Test]
56 | public async Task OptionTokenHandlesParamsArguments()
57 | {
58 | var code = """
59 | ConsoleApp.Run(args, ([Argument] string path, bool dryRun, params string[] extras) =>
60 | {
61 | Console.Write($"{dryRun}:{path}:{string.Join("|", extras)}");
62 | });
63 | """;
64 |
65 | await verifier.Execute(code, "--dry-run path.txt --extras src.txt dst.txt", "True:path.txt:src.txt|dst.txt");
66 | await verifier.Execute(code, "--dry-run path.txt", "True:path.txt:");
67 | }
68 |
69 | [Test]
70 | public async Task ArgumentAllowsLeadingDashValue()
71 | {
72 | var code = """
73 | ConsoleApp.Run(args, ([Argument] int count, bool dryRun) =>
74 | {
75 | Console.Write((count, dryRun).ToString());
76 | });
77 | """;
78 |
79 | await verifier.Execute(code, "-5 --dry-run", "(-5, True)");
80 | await verifier.Execute(code, "-5", "(-5, False)");
81 | }
82 |
83 | [Test]
84 | public async Task SyncRunShouldFailed()
85 | {
86 | await Assert.That(verifier.Error("""
87 | ConsoleApp.Log = x => Console.Write(x);
88 | ConsoleApp.Run(args, (int x) => { Console.Write((x)); });
89 | """, "--x").Stdout).Contains("Argument 'x' failed to parse");
90 |
91 | }
92 |
93 | [Test]
94 | public async Task EnumErrorShowsValidValues()
95 | {
96 | var result = verifier.Error("""
97 | ConsoleApp.Log = x => Console.Write(x);
98 | ConsoleApp.Run(args, (Fruit fruit) => { Console.Write(fruit); });
99 |
100 | enum Fruit
101 | {
102 | Orange, Grape, Apple
103 | }
104 | """, "--fruit Potato");
105 |
106 | await Assert.That(result.Stdout).Contains("Argument 'fruit' is invalid. Provided value: Potato. Valid values: Orange, Grape, Apple");
107 | await Assert.That(result.ExitCode).IsEqualTo(1);
108 | }
109 |
110 | [Test]
111 | public async Task MissingArgument()
112 | {
113 | var result = verifier.Error("""
114 | ConsoleApp.Log = x => Console.Write(x);
115 | ConsoleApp.Run(args, (int x, int y) => { Console.Write((x + y)); });
116 | """, "--x 10 y 20");
117 | await Assert.That(result.Stdout).Contains("Argument 'y' is not recognized.");
118 | await Assert.That(result.ExitCode).IsEqualTo(1);
119 | }
120 |
121 | [Test]
122 | public async Task ValidateOne()
123 | {
124 | var expected = """
125 | The field x must be between 1 and 10.
126 |
127 | """;
128 |
129 | var exitCode = await verifier.Execute("""
130 | ConsoleApp.Log = x => Console.Write(x);
131 | ConsoleApp.Run(args, ([Range(1, 10)]int x, [Range(100, 200)]int y) => { Console.Write((x + y)); });
132 | """, "--x 100 --y 140", expected);
133 |
134 | await Assert.That(exitCode).IsEqualTo(1);
135 | }
136 |
137 | [Test]
138 | public async Task ValidateTwo()
139 | {
140 | var expected = """
141 | The field x must be between 1 and 10.
142 | The field y must be between 100 and 200.
143 |
144 | """;
145 |
146 | var exitCode = await verifier.Execute("""
147 | ConsoleApp.Log = x => Console.Write(x);
148 | ConsoleApp.Run(args, ([Range(1, 10)]int x, [Range(100, 200)]int y) => { Console.Write((x + y)); });
149 | """, "--x 100 --y 240", expected);
150 |
151 | await Assert.That(exitCode).IsEqualTo(1);
152 | }
153 | [Test]
154 | public async Task Parameters()
155 | {
156 | await verifier.Execute("""
157 | ConsoleApp.Log = x => Console.Write(x);
158 | ConsoleApp.Run(args, (int foo, string bar, Fruit ft, bool flag, Half half, int? itt, Takoyaki.Obj obj) =>
159 | {
160 | Console.Write(foo);
161 | Console.Write(bar);
162 | Console.Write(ft);
163 | Console.Write(flag);
164 | Console.Write(half);
165 | Console.Write(itt);
166 | Console.Write(obj.Foo);
167 | });
168 |
169 | enum Fruit
170 | {
171 | Orange, Grape, Apple
172 | }
173 |
174 | namespace Takoyaki
175 | {
176 | public class Obj
177 | {
178 | public int Foo { get; set; }
179 | }
180 | }
181 | """, "--foo 10 --bar aiueo --ft Grape --flag --half 1.3 --itt 99 --obj {\"Foo\":1999}", "10aiueoGrapeTrue1.3991999");
182 | }
183 |
184 | [Test]
185 | public async Task ValidateClass()
186 | {
187 | var expected = """
188 | The field value must be between 0 and 1.
189 |
190 | """;
191 |
192 | await verifier.Execute("""
193 | ConsoleApp.Log = x => Console.Write(x);
194 | var app = ConsoleApp.Create();
195 | app.Add();
196 | app.Run(args);
197 |
198 | public class Test
199 | {
200 | public async Task Show(string aaa, [Range(0, 1)] double value) => ConsoleApp.Log($"{value}");
201 | }
202 |
203 | """, "show --aaa foo --value 100", expected);
204 |
205 | }
206 |
207 | [Test]
208 | public async Task StringEscape()
209 | {
210 | var code = """
211 | var app = ConsoleApp.Create();
212 | app.Add();
213 | app.Run(args);
214 |
215 | public class MyCommands
216 | {
217 | [Command("Error1")]
218 | public async Task Error1(string msg = @"\")
219 | {
220 | Console.Write(msg);
221 | }
222 | [Command("Error2")]
223 | public async Task Error2(string msg = "\\")
224 | {
225 | Console.Write(msg);
226 | }
227 | [Command("Output")]
228 | public async Task Output(string msg = @"\\")
229 | {
230 | Console.Write(msg);
231 | }
232 | }
233 | """;
234 |
235 | await verifier.Execute(code, "Error1", @"\");
236 | await verifier.Execute(code, "Error2", "\\");
237 | await verifier.Execute(code, "Output", @"\\");
238 |
239 | // lambda
240 |
241 | await verifier.Execute("""
242 | ConsoleApp.Run(args, (string msg = @"\") => Console.Write(msg));
243 | """, "", @"\");
244 |
245 | await verifier.Execute("""
246 | ConsoleApp.Run(args, (string msg = "\\") => Console.Write(msg));
247 | """, "", "\\");
248 |
249 | await verifier.Execute("""
250 | ConsoleApp.Run(args, (string msg = @"\\") => Console.Write(msg));
251 | """, "", @"\\");
252 | }
253 |
254 | [Test]
255 | public async Task ShortNameAlias()
256 | {
257 | var code = """
258 | var app = ConsoleApp.Create();
259 | app.Add();
260 | app.Run(args);
261 |
262 | public class FileCommand
263 | {
264 | /// Outputs the provided file name.
265 | /// -i, InputFile
266 | [Command("")]
267 | public async Task Run(string inputFile) => Console.Write(inputFile);
268 | }
269 | """;
270 |
271 | await verifier.Execute(code, "--input-file sample.txt", "sample.txt");
272 | await verifier.Execute(code, "-i sample.txt", "sample.txt");
273 | }
274 |
275 | [Test]
276 | public async Task ShortNameAndLongNameAlias()
277 | {
278 | var code = """
279 | var app = ConsoleApp.Create();
280 | app.Add();
281 | app.Run(args);
282 |
283 | public class FileCommand
284 | {
285 | /// Outputs the provided file name.
286 | /// -i|--input, InputFile
287 | [Command("")]
288 | public async Task Run(string inputFile) => Console.Write(inputFile);
289 | }
290 | """;
291 |
292 | await verifier.Execute(code, "--input-file sample.txt", "sample.txt");
293 | await verifier.Execute(code, "--input sample.txt", "sample.txt");
294 | await verifier.Execute(code, "-i sample.txt", "sample.txt");
295 | }
296 |
297 | [Test]
298 | public async Task ArgumentLastParams()
299 | {
300 | var code = """
301 | ConsoleApp.Run(args, (string opt1, [Argument]params string[] args) =>
302 | {
303 | Console.Write($"{opt1}, {string.Join("|", args)}");
304 | });
305 | """;
306 |
307 | await verifier.Execute(code, "--opt1 abc a b c d", "abc, a|b|c|d");
308 | }
309 |
310 | [Test]
311 | public async Task RunAndRunAsyncOverloads()
312 | {
313 | await verifier.Execute("""
314 | ConsoleApp.Run(args, () => Console.Write("sync"));
315 | """, "", "sync");
316 |
317 | await verifier.Execute("""
318 | ConsoleApp.Run(args, async () => Console.Write("async"));
319 | """, "", "async");
320 |
321 | await verifier.Execute("""
322 | await ConsoleApp.RunAsync(args, () => Console.Write("sync"));
323 | """, "", "sync");
324 |
325 | await verifier.Execute("""
326 | await ConsoleApp.RunAsync(args, async () => Console.Write("async"));
327 | """, "", "async");
328 | }
329 |
330 | [Test]
331 | public async Task RunAndRunAsyncOverloadsWithCancellationToken()
332 | {
333 | await verifier.Execute("""
334 | ConsoleApp.Run(args, (CancellationToken cancellationToken) => Console.Write("sync"));
335 | """, "", "sync");
336 |
337 | await verifier.Execute("""
338 | ConsoleApp.Run(args, async (CancellationToken cancellationToken) => Console.Write("async"));
339 | """, "", "async");
340 |
341 | await verifier.Execute("""
342 | await ConsoleApp.RunAsync(args, (CancellationToken cancellationToken) => Console.Write("sync"));
343 | """, "", "sync");
344 |
345 | await verifier.Execute("""
346 | await ConsoleApp.RunAsync(args, async (CancellationToken cancellationToken) => Console.Write("async"));
347 | """, "", "async");
348 | }
349 | }
350 |
--------------------------------------------------------------------------------
/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs:
--------------------------------------------------------------------------------
1 | using ConsoleAppFramework;
2 | using Microsoft.CodeAnalysis;
3 | using Microsoft.CodeAnalysis.CSharp;
4 | using Microsoft.CodeAnalysis.Diagnostics;
5 | using System.Collections.Immutable;
6 | using System.Diagnostics.CodeAnalysis;
7 | using System.Reflection;
8 | using System.Runtime.CompilerServices;
9 | using System.Runtime.Loader;
10 |
11 | public static class CSharpGeneratorRunner
12 | {
13 | static Compilation baseCompilation = default!;
14 |
15 | [ModuleInitializer]
16 | public static void InitializeCompilation()
17 | {
18 | var globalUsings = """
19 | global using System;
20 | global using System.Threading;
21 | global using System.Threading.Tasks;
22 | global using System.ComponentModel.DataAnnotations;
23 | global using ConsoleAppFramework;
24 | """;
25 |
26 | var references = AppDomain.CurrentDomain.GetAssemblies()
27 | .Where(x => !x.IsDynamic && !string.IsNullOrWhiteSpace(x.Location))
28 | .Select(x => MetadataReference.CreateFromFile(x.Location))
29 | .Concat([
30 | MetadataReference.CreateFromFile(typeof(Console).Assembly.Location), // System.Console.dll
31 | MetadataReference.CreateFromFile(typeof(IServiceProvider).Assembly.Location), // System.ComponentModel.dll
32 | MetadataReference.CreateFromFile(typeof(System.ComponentModel.DataAnnotations.RequiredAttribute).Assembly.Location), // System.ComponentModel.DataAnnotations
33 | MetadataReference.CreateFromFile(typeof(System.Text.Json.JsonDocument).Assembly.Location), // System.Text.Json.dll
34 | ]);
35 |
36 | var compilation = CSharpCompilation.Create("generatortest",
37 | references: references,
38 | syntaxTrees: [CSharpSyntaxTree.ParseText(globalUsings, path: "GlobalUsings.cs")],
39 | options: new CSharpCompilationOptions(OutputKind.ConsoleApplication, allowUnsafe: true)); // .exe
40 |
41 | baseCompilation = compilation;
42 | }
43 |
44 | public static (Compilation, ImmutableArray) RunGenerator([StringSyntax("C#-test")] string source, string[]? preprocessorSymbols = null, AnalyzerConfigOptionsProvider? options = null)
45 | {
46 | if (preprocessorSymbols == null)
47 | {
48 | preprocessorSymbols = new[] { "NET8_0_OR_GREATER" };
49 | }
50 | var parseOptions = new CSharpParseOptions(LanguageVersion.CSharp13, preprocessorSymbols: preprocessorSymbols); // 13
51 |
52 | var driver = CSharpGeneratorDriver.Create(new ConsoleAppGenerator()).WithUpdatedParseOptions(parseOptions);
53 | if (options != null)
54 | {
55 | driver = (Microsoft.CodeAnalysis.CSharp.CSharpGeneratorDriver)driver.WithUpdatedAnalyzerConfigOptions(options);
56 | }
57 |
58 | // overwrite System.Console.Write/WriteLine, Environment.ExitCode to capture output
59 | var captureStaticCode = """
60 | public static class Console
61 | {
62 | static global::System.IO.TextWriter textWriter = default!;
63 |
64 | public static void SetOut(global::System.IO.TextWriter textWriter)
65 | {
66 | Console.textWriter = textWriter;
67 | }
68 |
69 | public static void Write(object value)
70 | {
71 | textWriter.Write(value);
72 | }
73 |
74 | public static void WriteLine(object value)
75 | {
76 | textWriter.WriteLine(value);
77 | }
78 | }
79 |
80 | namespace ConsoleAppFramework
81 | {
82 | public static class Environment
83 | {
84 | public static int ExitCode { get; set; }
85 | }
86 | }
87 | """;
88 | var compilation = baseCompilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(source, parseOptions), CSharpSyntaxTree.ParseText(captureStaticCode, parseOptions));
89 |
90 | driver.RunGeneratorsAndUpdateCompilation(compilation, out var newCompilation, out var diagnostics);
91 | return (newCompilation, diagnostics);
92 | }
93 |
94 | public static (Compilation Compilation, ImmutableArray Diagnostics, string Stdout, int ExitCode) CompileAndExecute(string source, string[] args, string[]? preprocessorSymbols = null, AnalyzerConfigOptionsProvider? options = null)
95 | {
96 | var (compilation, diagnostics) = RunGenerator(source, preprocessorSymbols, options);
97 |
98 | using var ms = new MemoryStream();
99 | var emitResult = compilation.Emit(ms);
100 | if (!emitResult.Success)
101 | {
102 | throw new InvalidOperationException("Emit Failed\r\n" + string.Join("\r\n", emitResult.Diagnostics.Select(x => x.ToString())));
103 | }
104 |
105 | ms.Position = 0;
106 |
107 | var stringWriter = new StringWriter();
108 |
109 | // load and invoke Main(args)
110 | var loadContext = new AssemblyLoadContext("source-generator", isCollectible: true); // isCollectible to support Unload
111 | var assembly = loadContext.LoadFromStream(ms);
112 |
113 | assembly.GetType("Console")!.InvokeMember("SetOut", BindingFlags.Public | BindingFlags.Static | BindingFlags.InvokeMethod, null, null, [stringWriter]);
114 |
115 | assembly.EntryPoint!.Invoke(null, [args]);
116 |
117 | var exitCode = (int)assembly.GetType("ConsoleAppFramework.Environment")!.GetProperty("ExitCode")!.GetValue(null)!;
118 |
119 | loadContext.Unload();
120 |
121 | return (compilation, diagnostics, stringWriter.ToString(), exitCode);
122 | }
123 |
124 | public static (string Key, string Reasons)[][] GetIncrementalGeneratorTrackedStepsReasons(string keyPrefixFilter, params string[] sources)
125 | {
126 | var parseOptions = new CSharpParseOptions(LanguageVersion.CSharp13); // 13
127 | var driver = CSharpGeneratorDriver.Create(
128 | [new ConsoleAppGenerator().AsSourceGenerator()],
129 | driverOptions: new GeneratorDriverOptions(IncrementalGeneratorOutputKind.None, trackIncrementalGeneratorSteps: true))
130 | .WithUpdatedParseOptions(parseOptions);
131 |
132 | var generatorResults = sources
133 | .Select(source =>
134 | {
135 | var compilation = baseCompilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(source, parseOptions));
136 | driver = driver.RunGenerators(compilation);
137 | return driver.GetRunResult().Results[0];
138 | })
139 | .ToArray();
140 |
141 | var reasons = generatorResults
142 | .Select(x => x.TrackedSteps
143 | .Where(x => x.Key.StartsWith(keyPrefixFilter) || x.Key == "SourceOutput")
144 | .Select(x =>
145 | {
146 | if (x.Key == "SourceOutput")
147 | {
148 | var values = x.Value.Where(x => x.Inputs[0].Source.Name?.StartsWith(keyPrefixFilter) ?? false);
149 | return (
150 | x.Key,
151 | Reasons: string.Join(", ", values.SelectMany(x => x.Outputs).Select(x => x.Reason).ToArray())
152 | );
153 | }
154 | else
155 | {
156 | return (
157 | Key: x.Key.Substring(keyPrefixFilter.Length),
158 | Reasons: string.Join(", ", x.Value.SelectMany(x => x.Outputs).Select(x => x.Reason).ToArray())
159 | );
160 | }
161 | })
162 | .OrderBy(x => x.Key)
163 | .ToArray())
164 | .ToArray();
165 |
166 | return reasons;
167 | }
168 | }
169 |
170 | public class VerifyHelper(string idPrefix)
171 | {
172 | public async Task Ok([StringSyntax("C#-test")] string code, [CallerArgumentExpression("code")] string? codeExpr = null)
173 | {
174 | Console.WriteLine(codeExpr!);
175 |
176 | var (compilation, diagnostics) = CSharpGeneratorRunner.RunGenerator(code);
177 | foreach (var item in diagnostics)
178 | {
179 | Console.WriteLine(item.ToString());
180 | }
181 | OutputGeneratedCode(compilation);
182 |
183 | await Assert.That(diagnostics.Length).IsZero();
184 | }
185 |
186 | public async Task Verify(int id, [StringSyntax("C#-test")] string code, string diagnosticsCodeSpan, [CallerArgumentExpression("code")] string? codeExpr = null)
187 | {
188 | Console.WriteLine(codeExpr!);
189 |
190 | var (compilation, diagnostics) = CSharpGeneratorRunner.RunGenerator(code);
191 | foreach (var item in diagnostics)
192 | {
193 | Console.WriteLine(item.ToString());
194 | }
195 | OutputGeneratedCode(compilation);
196 |
197 | await Assert.That(diagnostics.Length).IsEqualTo(1);
198 | await Assert.That(diagnostics[0].Id).IsEqualTo(idPrefix + id.ToString("000"));
199 |
200 | var text = GetLocationText(diagnostics[0], compilation.SyntaxTrees);
201 | await Assert.That(text).IsEqualTo(diagnosticsCodeSpan);
202 | }
203 |
204 | public (string, string)[] Verify([StringSyntax("C#-test")] string code, [CallerArgumentExpression("code")] string? codeExpr = null)
205 | {
206 | Console.WriteLine(codeExpr!);
207 |
208 | var (compilation, diagnostics) = CSharpGeneratorRunner.RunGenerator(code);
209 | OutputGeneratedCode(compilation);
210 | return diagnostics.Select(x => (x.Id, GetLocationText(x, compilation.SyntaxTrees))).ToArray();
211 | }
212 |
213 | // Execute and check stdout result
214 |
215 | public async Task Execute([StringSyntax("C#-test")] string code, string args, string expected, [CallerArgumentExpression("code")] string? codeExpr = null)
216 | {
217 | Console.WriteLine(codeExpr!);
218 |
219 | var (compilation, diagnostics, stdout, exitCode) = CSharpGeneratorRunner.CompileAndExecute(code, args == "" ? [] : args.Split(' '));
220 | foreach (var item in diagnostics)
221 | {
222 | Console.WriteLine(item.ToString());
223 | }
224 | OutputGeneratedCode(compilation);
225 |
226 | await Assert.That(stdout).IsEqualTo(expected);
227 | return exitCode;
228 | }
229 |
230 | public (string Stdout, int ExitCode) Error([StringSyntax("C#-test")] string code, string args, [CallerArgumentExpression("code")] string? codeExpr = null)
231 | {
232 | Console.WriteLine(codeExpr!);
233 |
234 | var (compilation, diagnostics, stdout, exitCode) = CSharpGeneratorRunner.CompileAndExecute(code, args == "" ? [] : args.Split(' '));
235 | foreach (var item in diagnostics)
236 | {
237 | Console.WriteLine(item.ToString());
238 | }
239 | OutputGeneratedCode(compilation);
240 |
241 | return (stdout, exitCode);
242 | }
243 |
244 | string GetLocationText(Diagnostic diagnostic, IEnumerable syntaxTrees)
245 | {
246 | var location = diagnostic.Location;
247 |
248 | var textSpan = location.SourceSpan;
249 | var sourceTree = location.SourceTree;
250 | if (sourceTree == null)
251 | {
252 | var lineSpan = location.GetLineSpan();
253 | if (lineSpan.Path == null) return "";
254 |
255 | sourceTree = syntaxTrees.FirstOrDefault(x => x.FilePath == lineSpan.Path);
256 | if (sourceTree == null) return "";
257 | }
258 |
259 | var text = sourceTree.GetText().GetSubText(textSpan).ToString();
260 | return text;
261 | }
262 |
263 | void OutputGeneratedCode(Compilation compilation)
264 | {
265 | foreach (var syntaxTree in compilation.SyntaxTrees)
266 | {
267 | // only shows ConsoleApp.Run/Builder generated code
268 | if (!syntaxTree.FilePath.Contains("g.cs")) continue;
269 | Console.WriteLine(syntaxTree.ToString());
270 | }
271 | }
272 | }
273 |
--------------------------------------------------------------------------------
/tests/ConsoleAppFramework.GeneratorTests/HelpTest.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Reflection;
5 | using System.Text;
6 | using System.Threading.Tasks;
7 |
8 | namespace ConsoleAppFramework.GeneratorTests;
9 |
10 | public class HelpTest
11 | {
12 | VerifyHelper verifier = new VerifyHelper("CAF");
13 |
14 | [Test]
15 | public async Task Version()
16 | {
17 | var version = GetEntryAssemblyVersion();
18 |
19 | await verifier.Execute(code: $$"""
20 | ConsoleApp.Log = x => Console.WriteLine(x);
21 | ConsoleApp.Run(args, (int x, int y) => { });
22 | """,
23 | args: "--version",
24 | expected: $$"""
25 | {{version}}
26 |
27 | """);
28 | // custom
29 | await verifier.Execute(code: $$"""
30 | ConsoleApp.Log = x => Console.WriteLine(x);
31 | ConsoleApp.Version = "9999.9999999abcdefg";
32 | ConsoleApp.Run(args, (int x, int y) => { });
33 | """,
34 | args: "--version",
35 | expected: """
36 | 9999.9999999abcdefg
37 |
38 | """);
39 | }
40 |
41 | [Test]
42 | public async Task VersionOnBuilder()
43 | {
44 | var version = GetEntryAssemblyVersion();
45 |
46 | await verifier.Execute(code: """
47 | ConsoleApp.Log = x => Console.WriteLine(x);
48 | var app = ConsoleApp.Create();
49 | app.Run(args);
50 | """,
51 | args: "--version",
52 | expected: $$"""
53 | {{version}}
54 |
55 | """);
56 | }
57 |
58 | [Test]
59 | public async Task Run()
60 | {
61 | await verifier.Execute(code: """
62 | ConsoleApp.Log = x => Console.WriteLine(x);
63 | ConsoleApp.Run(args, (int x, int y) => { });
64 | """,
65 | args: "--help",
66 | expected: """
67 | Usage: [options...] [-h|--help] [--version]
68 |
69 | Options:
70 | --x [Required]
71 | --y [Required]
72 |
73 | """);
74 | }
75 |
76 | [Test]
77 | public async Task RunVoid()
78 | {
79 | await verifier.Execute(code: """
80 | ConsoleApp.Log = x => Console.WriteLine(x);
81 | ConsoleApp.Run(args, () => { });
82 | """,
83 | args: "--help",
84 | expected: """
85 | Usage: [-h|--help] [--version]
86 |
87 | """);
88 | }
89 |
90 | [Test]
91 | public async Task RootOnly()
92 | {
93 | await verifier.Execute(code: """
94 | ConsoleApp.Log = x => Console.WriteLine(x);
95 | var app = ConsoleApp.Create();
96 | app.Add("", (int x, int y) => { });
97 | app.Run(args);
98 | """,
99 | args: "--help",
100 | expected: """
101 | Usage: [options...] [-h|--help] [--version]
102 |
103 | Options:
104 | --x [Required]
105 | --y [Required]
106 |
107 | """);
108 | }
109 |
110 | [Test]
111 | public async Task ListWithoutRoot()
112 | {
113 | var code = """
114 | ConsoleApp.Log = x => Console.WriteLine(x);
115 | var app = ConsoleApp.Create();
116 | app.Add("a", (int x, int y) => { });
117 | app.Add("ab", (int x, int y) => { });
118 | app.Add("a b c", (int x, int y) => { });
119 | app.Run(args);
120 | """;
121 | await verifier.Execute(code, args: "--help", expected: """
122 | Usage: [command] [-h|--help] [--version]
123 |
124 | Commands:
125 | a
126 | a b c
127 | ab
128 |
129 | """);
130 | }
131 |
132 | [Test]
133 | public async Task ListWithRoot()
134 | {
135 | var code = """
136 | ConsoleApp.Log = x => Console.WriteLine(x);
137 | var app = ConsoleApp.Create();
138 | app.Add("", (int x, int y) => { });
139 | app.Add("a", (int x, int y) => { });
140 | app.Add("ab", (int x, int y) => { });
141 | app.Add("a b c", (int x, int y) => { });
142 | app.Run(args);
143 | """;
144 | await verifier.Execute(code, args: "--help", expected: """
145 | Usage: [command] [options...] [-h|--help] [--version]
146 |
147 | Options:
148 | --x [Required]
149 | --y [Required]
150 |
151 | Commands:
152 | a
153 | a b c
154 | ab
155 |
156 | """);
157 | }
158 |
159 | [Test]
160 | public async Task NoArgsOnRootShowsSameHelpTextAsHelpWhenParametersAreRequired()
161 | {
162 | var code = """
163 | ConsoleApp.Log = x => Console.WriteLine(x);
164 | var app = ConsoleApp.Create();
165 | app.Add("", (int x, int y) => { });
166 | app.Add("a", (int x, int y) => { });
167 | app.Add("ab", (int x, int y) => { });
168 | app.Add("a b c", (int x, int y) => { });
169 | app.Run(args);
170 | """;
171 | var noArgsOutput = verifier.Error(code, "");
172 | var helpOutput = verifier.Error(code, "--help");
173 |
174 | await Assert.That(noArgsOutput).IsEqualTo(helpOutput);
175 | }
176 |
177 | [Test]
178 | public async Task SelectLeafHelp()
179 | {
180 | var code = """
181 | ConsoleApp.Log = x => Console.WriteLine(x);
182 | var app = ConsoleApp.Create();
183 | app.Add("", (int x, int y) => { });
184 | app.Add("a", (int x, int y) => { });
185 | app.Add("ab", (int x, int y) => { });
186 | app.Add("a b c", (int x, int y) => { });
187 | app.Run(args);
188 | """;
189 | await verifier.Execute(code, args: "a b c --help", expected: """
190 | Usage: a b c [options...] [-h|--help] [--version]
191 |
192 | Options:
193 | --x [Required]
194 | --y [Required]
195 |
196 | """);
197 | }
198 |
199 | [Test]
200 | public async Task Summary()
201 | {
202 | var code = """
203 | ConsoleApp.Log = x => Console.WriteLine(x);
204 | var app = ConsoleApp.Create();
205 | app.Add();
206 | app.Run(args);
207 |
208 | public class MyClass
209 | {
210 | ///
211 | /// hello my world.
212 | ///
213 | /// -f|-fb, my foo is not bar.
214 | public async Task HelloWorld(string fooBar)
215 | {
216 | Console.Write("Hello World! " + fooBar);
217 | }
218 | }
219 | """;
220 | await verifier.Execute(code, args: "--help", expected: """
221 | Usage: [command] [-h|--help] [--version]
222 |
223 | Commands:
224 | hello-world hello my world.
225 |
226 | """);
227 |
228 | await verifier.Execute(code, args: "hello-world --help", expected: """
229 | Usage: hello-world [options...] [-h|--help] [--version]
230 |
231 | hello my world.
232 |
233 | Options:
234 | -f, -fb, --foo-bar my foo is not bar. [Required]
235 |
236 | """);
237 | }
238 |
239 | [Test]
240 | public async Task ArgumentOnly()
241 | {
242 | await verifier.Execute(code: """
243 | ConsoleApp.Log = x => Console.WriteLine(x);
244 | ConsoleApp.Run(args, ([Argument]int x, [Argument]int y) => { });
245 | """,
246 | args: "--help",
247 | expected: """
248 | Usage: [arguments...] [-h|--help] [--version]
249 |
250 | Arguments:
251 | [0]
252 | [1]
253 |
254 | """);
255 | }
256 |
257 | [Test]
258 | public async Task ArgumentWithParams()
259 | {
260 | await verifier.Execute(code: """
261 | ConsoleApp.Log = x => Console.WriteLine(x);
262 | ConsoleApp.Run(args, ([Argument]int x, [Argument]int y, params string[] yyy) => { });
263 | """,
264 | args: "--help",
265 | expected: """
266 | Usage: [arguments...] [options...] [-h|--help] [--version]
267 |
268 | Arguments:
269 | [0]
270 | [1]
271 |
272 | Options:
273 | --yyy ...
274 |
275 | """);
276 | }
277 |
278 | // Params
279 |
280 | [Test]
281 | public async Task Nullable()
282 | {
283 | await verifier.Execute(code: """
284 | ConsoleApp.Log = x => Console.WriteLine(x);
285 | ConsoleApp.Run(args, (int? x = null, string? y = null) => { });
286 | """,
287 | args: "--help",
288 | expected: """
289 | Usage: [options...] [-h|--help] [--version]
290 |
291 | Options:
292 | --x [Default: null]
293 | --y [Default: null]
294 |
295 | """);
296 | }
297 |
298 | [Test]
299 | public async Task EnumTest()
300 | {
301 | await verifier.Execute(code: """
302 | ConsoleApp.Log = x => Console.WriteLine(x);
303 | ConsoleApp.Run(args, (Fruit myFruit = Fruit.Apple, Fruit? moreFruit = null) => { });
304 |
305 | enum Fruit
306 | {
307 | Orange, Grape, Apple
308 | }
309 | """,
310 | args: "--help",
311 | expected: """
312 | Usage: [options...] [-h|--help] [--version]
313 |
314 | Options:
315 | --my-fruit [Default: Apple]
316 | --more-fruit [Default: null]
317 |
318 | """);
319 | }
320 |
321 | [Test]
322 | public async Task Summary2()
323 | {
324 | var code = """
325 | ConsoleApp.Log = x => Console.WriteLine(x);
326 | var app = ConsoleApp.Create();
327 | app.Add();
328 | app.Run(args);
329 |
330 | public class MyClass
331 | {
332 | ///
333 | /// hello my world.
334 | ///
335 | /// -b, my boo is not boo.
336 | /// -f|-fb, my foo, is not bar.
337 | public async Task HelloWorld([Argument]int boo, string fooBar)
338 | {
339 | Console.Write("Hello World! " + fooBar);
340 | }
341 | }
342 | """;
343 | await verifier.Execute(code, args: "hello-world --help", expected: """
344 | Usage: hello-world [arguments...] [options...] [-h|--help] [--version]
345 |
346 | hello my world.
347 |
348 | Arguments:
349 | [0] my boo is not boo.
350 |
351 | Options:
352 | -f, -fb, --foo-bar my foo, is not bar. [Required]
353 |
354 | """);
355 | }
356 |
357 | [Test]
358 | public async Task HideDefaultValue()
359 | {
360 | var code = """
361 | ConsoleApp.Log = x => Console.WriteLine(x);
362 | ConsoleApp.Run(args, Commands.Hello);
363 |
364 | static class Commands
365 | {
366 | ///
367 | /// Display Hello.
368 | ///
369 | /// -m, Message to show.
370 | public static void Hello([HideDefaultValue]string message = "ConsoleAppFramework") => Console.Write($"Hello, {message}");
371 | }
372 | """;
373 | await verifier.Execute(code, args: "--help", expected: """
374 | Usage: [options...] [-h|--help] [--version]
375 |
376 | Display Hello.
377 |
378 | Options:
379 | -m, --message Message to show.
380 |
381 | """);
382 | }
383 |
384 | [Test]
385 | public async Task GlobalOptions()
386 | {
387 | var code = """
388 | ConsoleApp.Log = x => Console.WriteLine(x);
389 | var app = ConsoleApp.Create();
390 |
391 | app.ConfigureGlobalOptions((ref ConsoleApp.GlobalOptionsBuilder builder) =>
392 | {
393 | var p = builder.AddGlobalOption("--parameter", "param global", defaultValue: 1000);
394 | var d = builder.AddGlobalOption(description: "run dry dry", name: "--dry-run");
395 | var r = builder.AddRequiredGlobalOption("--p2|--p3", "param 2");
396 | return (p, d, r);
397 | });
398 |
399 | app.Add("", (int x, int y) => { });
400 | app.Add("a", (int x, int y) => { });
401 | app.Add("ab", (int x, int y) => { });
402 | app.Add("a b c", (int x, int y) => { });
403 | app.Run(args);
404 | """;
405 |
406 | await verifier.Execute(code, args: "a --help", expected: """
407 | Usage: a [options...] [-h|--help] [--version]
408 |
409 | Options:
410 | --x [Required]
411 | --y [Required]
412 | --parameter param global [Default: 1000]
413 | --dry-run run dry dry
414 | --p2, --p3 param 2 [Required]
415 |
416 | """);
417 | }
418 |
419 | private static string GetEntryAssemblyVersion()
420 | {
421 | var version = System.Reflection.Assembly.GetEntryAssembly()?.GetCustomAttribute()?.InformationalVersion;
422 |
423 | if (version == null)
424 | {
425 | return "1.0.0";
426 | }
427 |
428 | // Trim SourceRevisionId (SourceLink feature is enabled by default when using .NET SDK 8 or later)
429 | var i = version.IndexOf('+');
430 | if (i != -1)
431 | {
432 | version = version.Substring(0, i);
433 | }
434 |
435 | return version;
436 | }
437 |
438 | [Test]
439 | public async Task CommandAlias()
440 | {
441 | var code = """
442 | ConsoleApp.Log = x => Console.WriteLine(x);
443 | var app = ConsoleApp.Create();
444 |
445 | app.Add("build|b", () => { Console.Write("build ok"); });
446 | app.Add("test|t", () => { Console.Write("test ok"); });
447 | app.Add();
448 |
449 | app.Run(args);
450 |
451 | public class Commands
452 | {
453 | /// Analyze the current package and report errors, but don't build object files.
454 | [Command("check|c")]
455 | public async Task Check() { Console.Write("check ok"); }
456 |
457 | /// Build this packages's and its dependencies' documenation.
458 | [Command("doc|d")]
459 | public async Task Doc() { Console.Write("doc ok"); }
460 | }
461 | """;
462 |
463 | await verifier.Execute(code, "--help", """
464 | Usage: [command] [-h|--help] [--version]
465 |
466 | Commands:
467 | build, b
468 | check, c Analyze the current package and report errors, but don't build object files.
469 | doc, d Build this packages's and its dependencies' documenation.
470 | test, t
471 |
472 | """);
473 | }
474 | }
475 |
--------------------------------------------------------------------------------
/src/ConsoleAppFramework/CommandHelpBuilder.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 | using System.Text;
3 |
4 | namespace ConsoleAppFramework;
5 |
6 | public static class CommandHelpBuilder
7 | {
8 | public static string BuildRootHelpMessage(Command command)
9 | {
10 | return BuildHelpMessageCore(command, showCommandName: false, showCommand: false);
11 | }
12 |
13 | public static string BuildRootHelpMessage(Command[] commands)
14 | {
15 | var sb = new StringBuilder();
16 |
17 | var rootCommand = commands.FirstOrDefault(x => x.IsRootCommand);
18 | var withoutRoot = commands.Where(x => !x.IsRootCommand).ToArray();
19 |
20 | if (rootCommand != null && withoutRoot.Length == 0)
21 | {
22 | return BuildRootHelpMessage(commands[0]);
23 | }
24 |
25 | if (rootCommand != null)
26 | {
27 | sb.AppendLine(BuildHelpMessageCore(rootCommand, false, withoutRoot.Length != 0));
28 | }
29 | else
30 | {
31 | sb.AppendLine("Usage: [command] [-h|--help] [--version]");
32 | sb.AppendLine();
33 | }
34 |
35 | if (withoutRoot.Length == 0) return sb.ToString();
36 |
37 | var helpDefinitions = withoutRoot.OrderBy(x => x.Name).ToArray();
38 |
39 | var list = BuildMethodListMessage(helpDefinitions, out _);
40 | sb.Append(list);
41 |
42 | return sb.ToString();
43 | }
44 |
45 | public static string BuildCommandHelpMessage(Command command)
46 | {
47 | return BuildHelpMessageCore(command, showCommandName: command.Name != "", showCommand: false);
48 | }
49 |
50 | public static string BuildCliSchema(IEnumerable commands)
51 | {
52 | return "return new CommandHelpDefinition[] {\n"
53 | + string.Join(", \n", commands.Select(x => CreateCommandHelpDefinition(x).ToCliSchema()))
54 | + "\n};";
55 | }
56 |
57 | static string BuildHelpMessageCore(Command command, bool showCommandName, bool showCommand)
58 | {
59 | var definition = CreateCommandHelpDefinition(command);
60 |
61 | var sb = new StringBuilder();
62 |
63 | sb.AppendLine(BuildUsageMessage(definition, showCommandName, showCommand));
64 |
65 | if (!string.IsNullOrEmpty(definition.Description))
66 | {
67 | sb.AppendLine();
68 | sb.AppendLine(definition.Description);
69 | }
70 |
71 | if (definition.Options.Any())
72 | {
73 | var hasArgument = definition.Options.Any(x => x.Index.HasValue);
74 | var hasNoHiddenOptions = definition.Options.Any(x => !x.Index.HasValue && !x.IsHidden);
75 |
76 | if (hasArgument)
77 | {
78 | sb.AppendLine();
79 | sb.AppendLine(BuildArgumentsMessage(definition));
80 | }
81 |
82 | if (hasNoHiddenOptions)
83 | {
84 | sb.AppendLine();
85 | sb.AppendLine(BuildOptionsMessage(definition));
86 | }
87 | }
88 |
89 | return sb.ToString();
90 | }
91 |
92 | static string BuildUsageMessage(CommandHelpDefinition definition, bool showCommandName, bool showCommand)
93 | {
94 | var sb = new StringBuilder();
95 | sb.Append($"Usage:");
96 |
97 | if (showCommandName)
98 | {
99 | sb.Append($" {definition.CommandName}");
100 | }
101 |
102 | if (showCommand)
103 | {
104 | sb.Append(" [command]");
105 | }
106 |
107 | if (definition.Options.Any(x => x.Index.HasValue))
108 | {
109 | sb.Append(" [arguments...]");
110 | }
111 |
112 | if (definition.Options.Any(x => !x.Index.HasValue && !x.IsHidden))
113 | {
114 | sb.Append(" [options...]");
115 | }
116 |
117 | sb.Append(" [-h|--help] [--version]");
118 |
119 | return sb.ToString();
120 | }
121 |
122 | static string BuildArgumentsMessage(CommandHelpDefinition definition)
123 | {
124 | var argumentsFormatted = definition.Options
125 | .Where(x => x.Index.HasValue)
126 | .Select(x => (Argument: $"[{x.Index}] {x.FormattedValueTypeName}", x.Description))
127 | .ToArray();
128 |
129 | if (!argumentsFormatted.Any()) return string.Empty;
130 |
131 | var maxWidth = argumentsFormatted.Max(x => x.Argument.Length);
132 |
133 | var sb = new StringBuilder();
134 |
135 | sb.AppendLine("Arguments:");
136 | var first = true;
137 | foreach (var arg in argumentsFormatted)
138 | {
139 | if (first)
140 | {
141 | first = false;
142 | }
143 | else
144 | {
145 | sb.AppendLine();
146 | }
147 | var padding = maxWidth - arg.Argument.Length;
148 |
149 | sb.Append(" ");
150 | sb.Append(arg.Argument);
151 | if (!string.IsNullOrEmpty(arg.Description))
152 | {
153 | for (var i = 0; i < padding; i++)
154 | {
155 | sb.Append(' ');
156 | }
157 |
158 | sb.Append(" ");
159 | sb.Append(arg.Description);
160 | }
161 | }
162 |
163 | return sb.ToString();
164 | }
165 |
166 | static string BuildOptionsMessage(CommandHelpDefinition definition)
167 | {
168 | var optionsFormatted = definition.Options
169 | .Where(x => !x.Index.HasValue)
170 | .Where(x => !x.IsHidden)
171 | .Select(x => (Options: string.Join(", ", x.Options) + (x.IsFlag ? string.Empty : $" {x.FormattedValueTypeName}{(x.IsParams ? "..." : "")}"), x.Description, x.IsRequired, x.IsFlag, x.DefaultValue, x.IsDefaultValueHidden))
172 | .ToArray();
173 |
174 | if (!optionsFormatted.Any()) return string.Empty;
175 |
176 | var maxWidth = optionsFormatted.Max(x => x.Options.Length);
177 |
178 | var sb = new StringBuilder();
179 |
180 | sb.AppendLine("Options:");
181 | var first = true;
182 | foreach (var opt in optionsFormatted)
183 | {
184 | if (first)
185 | {
186 | first = false;
187 | }
188 | else
189 | {
190 | sb.AppendLine();
191 | }
192 |
193 | var options = opt.Options;
194 | var padding = maxWidth - options.Length;
195 |
196 | sb.Append(" ");
197 | sb.Append(options);
198 | for (var i = 0; i < padding; i++)
199 | {
200 | sb.Append(' ');
201 | }
202 |
203 | sb.Append(" ");
204 | sb.Append(opt.Description);
205 |
206 | // Flags are optional by default; leave them untagged.
207 | if (!opt.IsFlag)
208 | {
209 | if (opt.DefaultValue != null)
210 | {
211 | if (!opt.IsDefaultValueHidden)
212 | {
213 | sb.Append($" [Default: {opt.DefaultValue}]");
214 | }
215 | }
216 | else if (opt.IsRequired)
217 | {
218 | sb.Append($" [Required]");
219 | }
220 | }
221 | }
222 |
223 | return sb.ToString();
224 | }
225 |
226 | static string BuildMethodListMessage(IEnumerable commands, out int maxWidth)
227 | {
228 | var formatted = commands
229 | .Where(x => !x.IsHidden)
230 | .Select(x =>
231 | {
232 | return (Command: string.Join(", ", x.Name.Split('|')), x.Description);
233 | })
234 | .ToArray();
235 | maxWidth = formatted.Max(x => x.Command.Length);
236 |
237 | var sb = new StringBuilder();
238 |
239 | sb.AppendLine("Commands:");
240 | foreach (var item in formatted)
241 | {
242 | sb.Append(" ");
243 | sb.Append(item.Command);
244 | if (string.IsNullOrEmpty(item.Description))
245 | {
246 | sb.AppendLine();
247 | }
248 | else
249 | {
250 | var padding = maxWidth - item.Command.Length;
251 | for (var i = 0; i < padding; i++)
252 | {
253 | sb.Append(' ');
254 | }
255 |
256 | sb.Append(" ");
257 | sb.AppendLine(item.Description);
258 | }
259 | }
260 |
261 | return sb.ToString();
262 | }
263 |
264 | static CommandHelpDefinition CreateCommandHelpDefinition(Command descriptor)
265 | {
266 | var parameterDefinitions = new List();
267 |
268 | foreach (var item in descriptor.Parameters)
269 | {
270 | // ignore DI params.
271 | if (!item.IsParsable) continue;
272 |
273 | // -i, -input | [default=foo]...
274 |
275 | var index = item.ArgumentIndex == -1 ? null : (int?)item.ArgumentIndex;
276 | var options = new List();
277 | if (item.ArgumentIndex != -1)
278 | {
279 | options.Add($"[{item.ArgumentIndex}]");
280 | }
281 | else
282 | {
283 | // aliases first
284 | foreach (var alias in item.Aliases)
285 | {
286 | options.Add(alias);
287 | }
288 | if (item.Name != null)
289 | {
290 | options.Add("--" + item.Name);
291 | }
292 | }
293 |
294 | var description = item.Description;
295 | var isFlag = item.Type.SpecialType == Microsoft.CodeAnalysis.SpecialType.System_Boolean;
296 | var isParams = item.IsParams;
297 | var isHidden = item.IsHidden;
298 | var isDefaultValueHidden = item.IsDefaultValueHidden;
299 |
300 | var defaultValue = default(string);
301 | if (item.HasDefaultValue)
302 | {
303 | defaultValue = item.DefaultValue == null ? "null" : item.DefaultValueToString(castValue: false, enumIncludeTypeName: false);
304 | if (isFlag)
305 | {
306 | if (item.DefaultValue is true)
307 | {
308 | // bool option with true default value is not flag.
309 | isFlag = false;
310 | }
311 | else if (item.DefaultValue is false)
312 | {
313 | // false default value should be omitted for flag.
314 | defaultValue = null;
315 | }
316 | }
317 | }
318 |
319 | var paramTypeName = item.ToTypeShortString();
320 | parameterDefinitions.Add(new CommandOptionHelpDefinition(options.Distinct().ToArray(), description, paramTypeName, defaultValue, index, isFlag, isParams, isHidden, isDefaultValueHidden));
321 | }
322 |
323 | var commandName = descriptor.Name;
324 | return new CommandHelpDefinition(
325 | commandName,
326 | parameterDefinitions.ToArray(),
327 | descriptor.Description
328 | );
329 | }
330 |
331 | class CommandHelpDefinition
332 | {
333 | public string CommandName { get; }
334 | public CommandOptionHelpDefinition[] Options { get; }
335 | public string Description { get; }
336 |
337 | public CommandHelpDefinition(string command, CommandOptionHelpDefinition[] options, string description)
338 | {
339 | CommandName = command;
340 | Options = options;
341 | Description = description;
342 | }
343 |
344 | public string ToCliSchema()
345 | {
346 | var sb = new StringBuilder();
347 | sb.AppendLine($"new CommandHelpDefinition(");
348 | sb.AppendLine($" \"{EscapeString(CommandName)}\",");
349 | sb.AppendLine($" new CommandOptionHelpDefinition[]");
350 | sb.AppendLine($" {{");
351 |
352 | for (int i = 0; i < Options.Length; i++)
353 | {
354 | sb.Append(" ");
355 | sb.Append(Options[i].ToCliSchema());
356 | if (i < Options.Length - 1)
357 | {
358 | sb.AppendLine(",");
359 | }
360 | else
361 | {
362 | sb.AppendLine();
363 | }
364 | }
365 |
366 | sb.AppendLine($" }},");
367 | sb.AppendLine($" \"{EscapeString(Description)}\"");
368 | sb.Append($")");
369 |
370 | return sb.ToString();
371 | }
372 |
373 | private static string EscapeString(string str)
374 | {
375 | return str.Replace("\\", "\\\\")
376 | .Replace("\"", "\\\"")
377 | .Replace("\n", "\\n")
378 | .Replace("\r", "\\r")
379 | .Replace("\t", "\\t");
380 | }
381 | }
382 |
383 | class CommandOptionHelpDefinition
384 | {
385 | public string[] Options { get; }
386 | public string Description { get; }
387 | public string? DefaultValue { get; }
388 | public string ValueTypeName { get; }
389 | public int? Index { get; }
390 | public bool IsRequired => DefaultValue == null && !IsParams;
391 | public bool IsFlag { get; }
392 | public bool IsParams { get; }
393 | public bool IsHidden { get; }
394 | public bool IsDefaultValueHidden { get; }
395 | public string FormattedValueTypeName => "<" + ValueTypeName + ">";
396 |
397 | public CommandOptionHelpDefinition(string[] options, string description, string valueTypeName, string? defaultValue, int? index, bool isFlag, bool isParams, bool isHidden, bool isDefaultValueHidden)
398 | {
399 | Options = options;
400 | Description = description;
401 | ValueTypeName = valueTypeName;
402 | DefaultValue = defaultValue;
403 | Index = index;
404 | IsFlag = isFlag;
405 | IsParams = isParams;
406 | IsHidden = isHidden;
407 | IsDefaultValueHidden = isDefaultValueHidden;
408 | }
409 |
410 | public string ToCliSchema()
411 | {
412 | var optionsArray = string.Join(", ", Options.Select(o => $"\"{EscapeString(o)}\""));
413 | var defaultValueStr = DefaultValue == null ? "null" : $"\"{EscapeString(DefaultValue)}\"";
414 | var indexStr = Index.HasValue ? Index.Value.ToString() : "null";
415 |
416 | return $"new CommandOptionHelpDefinition(new[] {{ {optionsArray} }}, \"{EscapeString(Description)}\", \"{EscapeString(ValueTypeName)}\", {defaultValueStr}, {indexStr}, {IsFlag.ToString().ToLower()}, {IsParams.ToString().ToLower()}, {IsHidden.ToString().ToLower()}, {IsDefaultValueHidden.ToString().ToLower()})";
417 | }
418 |
419 | private static string EscapeString(string str)
420 | {
421 | return str.Replace("\\", "\\\\")
422 | .Replace("\"", "\\\"")
423 | .Replace("\n", "\\n")
424 | .Replace("\r", "\\r")
425 | .Replace("\t", "\\t");
426 | }
427 | }
428 | }
429 |
--------------------------------------------------------------------------------
/tests/ConsoleAppFramework.GeneratorTests/DiagnosticsTest.cs:
--------------------------------------------------------------------------------
1 | namespace ConsoleAppFramework.GeneratorTests;
2 |
3 | public class DiagnosticsTest
4 | {
5 | VerifyHelper verifier = new VerifyHelper("CAF");
6 |
7 | [Test]
8 | public async Task ArgumentCount()
9 | {
10 | await verifier.Verify(1, "ConsoleApp.Run(args);", "ConsoleApp.Run(args)");
11 | await verifier.Verify(1, "ConsoleApp.Run();", "ConsoleApp.Run()");
12 | await verifier.Verify(1, "ConsoleApp.Run(args, (int x, int y) => { }, 1000);", "ConsoleApp.Run(args, (int x, int y) => { }, 1000)");
13 | }
14 |
15 | [Test]
16 | public async Task InvalidReturnTypeFromLambda()
17 | {
18 | await verifier.Verify(2, "ConsoleApp.Run(args, string (int x, int y) => { return \"foo\"; })", "string");
19 | await verifier.Verify(2, "ConsoleApp.Run(args, int? (int x, int y) => { return -1; })", "int?");
20 | await verifier.Verify(2, "ConsoleApp.Run(args, Task (int x, int y) => { return Task.CompletedTask; })", "Task");
21 | await verifier.Verify(2, "ConsoleApp.Run(args, Task (int x, int y) => { return Task.FromResult(0); })", "Task");
22 | await verifier.Verify(2, "ConsoleApp.Run(args, async Task (int x, int y) => { return \"foo\"; })", "Task");
23 | await verifier.Verify(2, "ConsoleApp.Run(args, async ValueTask (int x, int y) => { })", "ValueTask");
24 | await verifier.Verify(2, "ConsoleApp.Run(args, async ValueTask (int x, int y) => { return -1; })", "ValueTask");
25 | await verifier.Ok("ConsoleApp.Run(args, (int x, int y) => { })");
26 | await verifier.Ok("ConsoleApp.Run(args, void (int x, int y) => { })");
27 | await verifier.Ok("ConsoleApp.Run(args, int (int x, int y) => { })");
28 | await verifier.Ok("ConsoleApp.Run(args, async Task (int x, int y) => { })");
29 | await verifier.Ok("ConsoleApp.Run(args, async Task (int x, int y) => { })");
30 | }
31 |
32 | [Test]
33 | public async Task InvalidReturnTypeFromMethodReference()
34 | {
35 | await verifier.Verify(3, "ConsoleApp.Run(args, Invoke); float Invoke(int x, int y) => 0.3f;", "float");
36 | await verifier.Verify(3, "ConsoleApp.Run(args, InvokeAsync); async Task InvokeAsync(int x, int y) => 0.3f;", "Task");
37 | await verifier.Ok("ConsoleApp.Run(args, Run); void Run(int x, int y) { };");
38 | await verifier.Ok("ConsoleApp.Run(args, Run); static void Run(int x, int y) { };");
39 | await verifier.Ok("ConsoleApp.Run(args, Run); int Run(int x, int y) => -1;");
40 | await verifier.Ok("ConsoleApp.Run(args, Run); async Task Run(int x, int y) { };");
41 | await verifier.Ok("ConsoleApp.Run(args, Run); async Task Run(int x, int y) => -1;");
42 | }
43 |
44 | [Test]
45 | public async Task RunAsyncValidation()
46 | {
47 | await verifier.Verify(2, "ConsoleApp.RunAsync(args, string (int x, int y) => { return \"foo\"; })", "string");
48 | await verifier.Verify(2, "ConsoleApp.RunAsync(args, int? (int x, int y) => { return -1; })", "int?");
49 | await verifier.Verify(2, "ConsoleApp.RunAsync(args, Task (int x, int y) => { return Task.CompletedTask; })", "Task");
50 | await verifier.Verify(2, "ConsoleApp.RunAsync(args, Task (int x, int y) => { return Task.FromResult(0); })", "Task");
51 | await verifier.Verify(2, "ConsoleApp.RunAsync(args, async Task (int x, int y) => { return \"foo\"; })", "Task");
52 | await verifier.Verify(2, "ConsoleApp.RunAsync(args, async ValueTask (int x, int y) => { })", "ValueTask");
53 | await verifier.Verify(2, "ConsoleApp.RunAsync(args, async ValueTask (int x, int y) => { return -1; })", "ValueTask");
54 | await verifier.Ok("ConsoleApp.RunAsync(args, (int x, int y) => { })");
55 | await verifier.Ok("ConsoleApp.RunAsync(args, void (int x, int y) => { })");
56 | await verifier.Ok("ConsoleApp.RunAsync(args, int (int x, int y) => { })");
57 | await verifier.Ok("ConsoleApp.RunAsync(args, async Task (int x, int y) => { })");
58 | await verifier.Ok("ConsoleApp.RunAsync(args, async Task (int x, int y) => { })");
59 |
60 | await verifier.Verify(3, "ConsoleApp.RunAsync(args, Invoke); float Invoke(int x, int y) => 0.3f;", "float");
61 | await verifier.Verify(3, "ConsoleApp.RunAsync(args, InvokeAsync); async Task InvokeAsync(int x, int y) => 0.3f;", "Task");
62 | await verifier.Ok("ConsoleApp.RunAsync(args, Run); void Run(int x, int y) { };");
63 | await verifier.Ok("ConsoleApp.RunAsync(args, Run); static void Run(int x, int y) { };");
64 | await verifier.Ok("ConsoleApp.RunAsync(args, Run); int Run(int x, int y) => -1;");
65 | await verifier.Ok("ConsoleApp.RunAsync(args, Run); async Task Run(int x, int y) { };");
66 | await verifier.Ok("ConsoleApp.RunAsync(args, Run); async Task Run(int x, int y) => -1;");
67 | }
68 |
69 | // v5.7.7 supports non-first argument parameters
70 | //[Fact]
71 | //public async Task Argument()
72 | //{
73 | // await verifier.Verify(4, "ConsoleApp.Run(args, (int x, [Argument]int y) => { })", "[Argument]int y");
74 | // await verifier.Verify(4, "ConsoleApp.Run(args, ([Argument]int x, int y, [Argument]int z) => { })", "[Argument]int z");
75 | // await verifier.Verify(4, "ConsoleApp.Run(args, Run); void Run(int x, [Argument]int y) { };", "[Argument]int y");
76 |
77 | // await verifier.Ok("ConsoleApp.Run(args, ([Argument]int x, [Argument]int y) => { })");
78 | // await verifier.Ok("ConsoleApp.Run(args, Run); void Run([Argument]int x, [Argument]int y) { };");
79 | //}
80 |
81 | [Test]
82 | public async Task FunctionPointerValidation()
83 | {
84 | await verifier.Verify(5, "unsafe { ConsoleApp.Run(args, &Run2); static void Run2([Range(1, 10)]int x, int y) { }; }", "[Range(1, 10)]int x");
85 |
86 | await verifier.Ok("unsafe { ConsoleApp.Run(args, &Run2); static void Run2(int x, int y) { }; }");
87 | }
88 |
89 | [Test]
90 | public async Task BuilderAddConstCommandName()
91 | {
92 | await verifier.Verify(6, """
93 | var builder = ConsoleApp.Create();
94 | var baz = "foo";
95 | builder.Add(baz, (int x, int y) => { } );
96 | """, "baz");
97 |
98 | await verifier.Ok("""
99 | var builder = ConsoleApp.Create();
100 | builder.Add("foo", (int x, int y) => { } );
101 | builder.Run(args);
102 | """);
103 | }
104 |
105 | [Test]
106 | public async Task DuplicateCommandName()
107 | {
108 | await verifier.Verify(7, """
109 | var builder = ConsoleApp.Create();
110 | builder.Add("foo", (int x, int y) => { } );
111 | builder.Add("foo", (int x, int y) => { } );
112 | """, "\"foo\"");
113 | }
114 |
115 | [Test]
116 | public async Task DuplicateCommandNameClass()
117 | {
118 | await verifier.Verify(7, """
119 | var builder = ConsoleApp.Create();
120 | builder.Add();
121 |
122 | public class MyClass
123 | {
124 | public async Task Do()
125 | {
126 | Console.Write("yeah:");
127 | }
128 |
129 | public async Task Do(int i)
130 | {
131 | Console.Write("yeah:");
132 | }
133 | }
134 | """, "builder.Add()");
135 |
136 | await verifier.Verify(7, """
137 | var builder = ConsoleApp.Create();
138 | builder.Add("do", (int x, int y) => { } );
139 | builder.Add();
140 | builder.Run(args);
141 |
142 | public class MyClass
143 | {
144 | public async Task Do()
145 | {
146 | Console.Write("yeah:");
147 | }
148 | }
149 | """, "builder.Add()");
150 | }
151 |
152 | [Test]
153 | public async Task AddInLoop()
154 | {
155 | var myClass = """
156 | public class MyClass
157 | {
158 | public async Task Do()
159 | {
160 | Console.Write("yeah:");
161 | }
162 | }
163 | """;
164 | await verifier.Verify(8, $$"""
165 | var builder = ConsoleApp.Create();
166 | while (true)
167 | {
168 | builder.Add();
169 | }
170 |
171 | {{myClass}}
172 | """, "builder.Add()");
173 |
174 | await verifier.Verify(8, $$"""
175 | var builder = ConsoleApp.Create();
176 | for (int i = 0; i < 10; i++)
177 | {
178 | builder.Add();
179 | }
180 |
181 | {{myClass}}
182 | """, "builder.Add()");
183 |
184 | await verifier.Verify(8, $$"""
185 | var builder = ConsoleApp.Create();
186 | do
187 | {
188 | builder.Add();
189 | } while(true);
190 |
191 | {{myClass}}
192 | """, "builder.Add()");
193 |
194 | await verifier.Verify(8, $$"""
195 | var builder = ConsoleApp.Create();
196 | foreach (var item in new[]{1,2,3})
197 | {
198 | builder.Add();
199 | }
200 |
201 | {{myClass}}
202 | """, "builder.Add()");
203 | }
204 |
205 | [Test]
206 | public async Task ErrorInBuilderAPI()
207 | {
208 | await verifier.Verify(3, $$"""
209 | var builder = ConsoleApp.Create();
210 | builder.Add();
211 |
212 | public class MyClass
213 | {
214 | public string Do()
215 | {
216 | Console.Write("yeah:");
217 | return "foo";
218 | }
219 | }
220 | """, "string");
221 |
222 | await verifier.Verify(3, $$"""
223 | var builder = ConsoleApp.Create();
224 | builder.Add();
225 |
226 | public class MyClass
227 | {
228 | public async Task Do()
229 | {
230 | Console.Write("yeah:");
231 | return "foo";
232 | }
233 | }
234 | """, "Task");
235 |
236 | await verifier.Verify(2, $$"""
237 | var builder = ConsoleApp.Create();
238 | builder.Add("foo", string (int x, int y) => { return "foo"; });
239 | """, "string");
240 |
241 | await verifier.Verify(2, $$"""
242 | var builder = ConsoleApp.Create();
243 | builder.Add("foo", async Task (int x, int y) => { return "foo"; });
244 | """, "Task");
245 | }
246 |
247 |
248 |
249 | [Test]
250 | public async Task RunAndFilter()
251 | {
252 | await verifier.Verify(9, """
253 | ConsoleApp.Run(args, Hello);
254 |
255 | [ConsoleAppFilter]
256 | void Hello()
257 | {
258 | }
259 |
260 | public class NopFilter(ConsoleAppFilter next)
261 | : ConsoleAppFilter(next)
262 | {
263 | public override Task InvokeAsync(CancellationToken cancellationToken)
264 | {
265 | return Next.InvokeAsync(cancellationToken);
266 | }
267 | }
268 | """, "ConsoleApp.Run(args, Hello)");
269 | }
270 |
271 | [Test]
272 | public async Task MultiConstructorFilter()
273 | {
274 | await verifier.Verify(10, """
275 | var app = ConsoleApp.Create();
276 | app.UseFilter();
277 | app.Add("", Hello);
278 | app.Run(args);
279 |
280 | void Hello()
281 | {
282 | }
283 |
284 | internal class NopFilter : ConsoleAppFilter
285 | {
286 | public NopFilter(ConsoleAppFilter next)
287 | :base(next)
288 | {
289 | }
290 |
291 | public NopFilter(string x, ConsoleAppFilter next)
292 | :base(next)
293 | {
294 | }
295 |
296 | public override Task InvokeAsync(CancellationToken cancellationToken)
297 | {
298 | return Next.InvokeAsync(cancellationToken);
299 | }
300 | }
301 | """, "NopFilter");
302 |
303 | await verifier.Verify(10, """
304 | var app = ConsoleApp.Create();
305 | app.Add();
306 | app.Run(args);
307 |
308 | [ConsoleAppFilter]
309 | public class Foo
310 | {
311 | public async Task Hello()
312 | {
313 | }
314 | }
315 |
316 | internal class NopFilter : ConsoleAppFilter
317 | {
318 | public NopFilter(ConsoleAppFilter next)
319 | :base(next)
320 | {
321 | }
322 |
323 | public NopFilter(string x, ConsoleAppFilter next)
324 | :base(next)
325 | {
326 | }
327 |
328 | public override Task InvokeAsync(CancellationToken cancellationToken)
329 | {
330 | return Next.InvokeAsync(cancellationToken);
331 | }
332 | }
333 | """, "ConsoleAppFilter");
334 |
335 | await verifier.Verify(10, """
336 | var app = ConsoleApp.Create();
337 | app.Add();
338 | app.Run(args);
339 |
340 | public class Foo
341 | {
342 | [ConsoleAppFilter]
343 | public async Task Hello()
344 | {
345 | }
346 | }
347 |
348 | internal class NopFilter : ConsoleAppFilter
349 | {
350 | public NopFilter(ConsoleAppFilter next)
351 | :base(next)
352 | {
353 | }
354 |
355 | public NopFilter(string x, ConsoleAppFilter next)
356 | :base(next)
357 | {
358 | }
359 |
360 | public override Task InvokeAsync(CancellationToken cancellationToken)
361 | {
362 | return Next.InvokeAsync(cancellationToken);
363 | }
364 | }
365 | """, "ConsoleAppFilter");
366 | }
367 |
368 |
369 | [Test]
370 | public async Task MultipleCtorClass()
371 | {
372 | await verifier.Verify(11, """
373 | var app = ConsoleApp.Create();
374 | app.Add();
375 | app.Run(args);
376 |
377 | public class Foo
378 | {
379 | public Foo() { }
380 | public Foo(int x) { }
381 |
382 | public async Task Hello()
383 | {
384 | }
385 | }
386 | """, "app.Add()");
387 | }
388 |
389 | [Test]
390 | public async Task PublicMethods()
391 | {
392 | await verifier.Verify(12, """
393 | var app = ConsoleApp.Create();
394 | app.Add();
395 | app.Run(args);
396 |
397 | public class Foo
398 | {
399 | public Foo() { }
400 | public Foo(int x) { }
401 |
402 | private void Hello()
403 | {
404 | }
405 | }
406 | """, "app.Add()");
407 | }
408 |
409 | [Test]
410 | public async Task AbstractNotAllow()
411 | {
412 | await verifier.Verify(13, """
413 | var app = ConsoleApp.Create();
414 | app.Add();
415 | app.Run(args);
416 |
417 | public abstract class Foo
418 | {
419 | public async Task Hello()
420 | {
421 | }
422 | }
423 | """, "app.Add()");
424 |
425 | await verifier.Verify(13, """
426 | var app = ConsoleApp.Create();
427 | app.Add();
428 | app.Run(args);
429 |
430 | public interface IFoo
431 | {
432 | void Hello();
433 | }
434 | """, "app.Add()");
435 | }
436 |
437 | [Test]
438 | public async Task DocCommentName()
439 | {
440 | await verifier.Verify(15, """
441 | var app = ConsoleApp.Create();
442 | app.Add();
443 | app.Run(args);
444 |
445 | public class Foo
446 | {
447 | /// foobarbaz!
448 | [Command("Error1")]
449 | public async Task Bar(string msg)
450 | {
451 | Console.WriteLine(msg);
452 | }
453 | }
454 |
455 | """, "Bar");
456 |
457 | }
458 |
459 | [Test]
460 | public async Task AsyncVoid()
461 | {
462 | await verifier.Verify(16, """
463 | var app = ConsoleApp.Create();
464 | app.Add();
465 | app.Run(args);
466 |
467 | public class MyCommands2
468 | {
469 | public async void Foo()
470 | {
471 | await Task.Yield();
472 | }
473 | }
474 |
475 | """, "async");
476 | }
477 |
478 | [Test]
479 | public async Task GlobalOptionsDuplicate()
480 | {
481 | await verifier.Verify(17, """
482 | var app = ConsoleApp.Create();
483 |
484 | app.ConfigureGlobalOptions((ref ConsoleApp.GlobalOptionsBuilder builder) =>
485 | {
486 | return new object();
487 | });
488 |
489 | app.ConfigureGlobalOptions((ref ConsoleApp.GlobalOptionsBuilder builder) =>
490 | {
491 | return new object();
492 | });
493 |
494 | app.Run(args);
495 | """, "app.ConfigureGlobalOptions");
496 | }
497 |
498 | [Test]
499 | public async Task GlobalOptionsInvalidType()
500 | {
501 | await verifier.Verify(18, """
502 | var app = ConsoleApp.Create();
503 |
504 | app.ConfigureGlobalOptions((ref ConsoleApp.GlobalOptionsBuilder builder) =>
505 | {
506 | builder.AddGlobalOption("foo");
507 | return new object();
508 | });
509 |
510 | app.Run(args);
511 | """, "builder.AddGlobalOption(\"foo\")");
512 | }
513 |
514 | }
515 |
--------------------------------------------------------------------------------