diff --git a/src/ConsoleAppFramework/ConsoleAppGenerator.cs b/src/ConsoleAppFramework/ConsoleAppGenerator.cs index 14d78c6f..4d794e2a 100644 --- a/src/ConsoleAppFramework/ConsoleAppGenerator.cs +++ b/src/ConsoleAppFramework/ConsoleAppGenerator.cs @@ -63,7 +63,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var expr = invocationExpression.Expression as MemberAccessExpressionSyntax; var methodName = expr?.Name.Identifier.Text; - if (methodName is "Add" or "UseFilter" or "Run" or "RunAsync") + if (methodName is "Add" or "SubcommandHelp" or "UseFilter" or "Run" or "RunAsync") { return true; } @@ -144,7 +144,7 @@ internal sealed class ArgumentAttribute : Attribute { } -[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] internal sealed class CommandAttribute : Attribute { public string Command { get; } @@ -457,6 +457,12 @@ public void Dispose() } } + public enum DisplayType + { + Default, + Hidden + } + internal partial struct ConsoleAppBuilder { public ConsoleAppBuilder() @@ -468,6 +474,11 @@ public void Add(string commandName, Delegate command) AddCore(commandName, command); } + public void SubcommandHelp(DisplayType displayType) + { + // Not Implemented yet + } + [System.Diagnostics.Conditional("DEBUG")] public void Add() { } diff --git a/src/ConsoleAppFramework/Parser.cs b/src/ConsoleAppFramework/Parser.cs index 019bf15f..18234b09 100644 --- a/src/ConsoleAppFramework/Parser.cs +++ b/src/ConsoleAppFramework/Parser.cs @@ -55,8 +55,32 @@ internal class Parser(DiagnosticReporter context, InvocationExpressionSyntax nod var genericName = (node.Expression as MemberAccessExpressionSyntax)?.Name as GenericNameSyntax; var genericType = genericName!.TypeArgumentList.Arguments[0]; - // Add(string commandPath) string? commandPath = null; + + if (genericName != null) + { + var className = genericName.TypeArgumentList.Arguments.First().ToString(); + + // Find the class declaration with the matching class name + var classDeclaration = model.SyntaxTree.GetRoot().DescendantNodes() + .OfType() + .FirstOrDefault(c => c.Identifier.Text == className); + + if (classDeclaration != null) + { + var commandAttribute = classDeclaration.AttributeLists + .SelectMany(al => al.Attributes) + .FirstOrDefault(a => a.Name.ToString() == "Command"); + + var attributeArgument = commandAttribute?.ArgumentList?.Arguments.FirstOrDefault()?.ToString().Trim('"'); + if (attributeArgument != null) + { + commandPath = attributeArgument; + } + } + } + + // Add(string commandPath) var args = node.ArgumentList.Arguments; if (node.ArgumentList.Arguments.Count == 1) { @@ -67,7 +91,14 @@ internal class Parser(DiagnosticReporter context, InvocationExpressionSyntax nod return []; } - commandPath = (commandName.Expression as LiteralExpressionSyntax)!.Token.ValueText; + if (commandPath == null) + { + commandPath = (commandName.Expression as LiteralExpressionSyntax)!.Token.ValueText; + } + else + { + context.ReportDiagnostic(DiagnosticDescriptors.DuplicateCommandName, commandName.GetLocation(), commandPath); + } } // T diff --git a/tests/ConsoleAppFramework.GeneratorTests/HelpTest.cs b/tests/ConsoleAppFramework.GeneratorTests/HelpTest.cs index 48778a28..1e3e392d 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/HelpTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/HelpTest.cs @@ -297,6 +297,534 @@ public void HelloWorld([Argument]int boo, string fooBar) hello my world. +Arguments: + [0] my boo is not boo. + +Options: + -f|-fb|--foo-bar my foo is not bar. (Required) + +"""); + } + + [Fact] + public void ClassCommandNoClassSummary() + { + var code = """ +var app = ConsoleApp.Create(); +app.Add("mc"); +app.Run(args); + +[Command("mc")] +public class MyClass +{ + /// + /// hello my world. + /// + /// -b, my boo is not boo. + /// -f|-fb, my foo is not bar. + public void HelloWorld([Argument]int boo, string fooBar) + { + Console.Write("Hello World! " + fooBar); + } +} +"""; + + verifier.Execute(code, args: "--help", expected: """ +Usage: mc [command] [options...] [-h|--help] [--version] + +Commands: + hello-world hello my world. + +"""); + verifier.Execute(code, args: "hello-world --help", expected: """ +Usage: hello-world [arguments...] [options...] [-h|--help] [--version] + +hello my world. + +Arguments: + [0] my boo is not boo. + +Options: + -f|-fb|--foo-bar my foo is not bar. (Required) + +"""); + } + + [Fact] + public void ClassCommandWithSummary() + { + var code = """ +var app = ConsoleApp.Create(); +app.Add(); +app.Run(args); + +/// +/// My class +/// +[Command("mc")] +public class MyClass +{ + /// + /// hello my world. + /// + /// -b, my boo is not boo. + /// -f|-fb, my foo is not bar. + public void HelloWorld([Argument]int boo, string fooBar) + { + Console.Write("Hello World! " + fooBar); + } +} +"""; + verifier.Execute(code, args: "--help", expected: """ +Usage: mc [command] [options...] [-h|--help] [--version] + +My class + +Commands: + hello-world hello my world. + +"""); + + verifier.Execute(code, args: "mc hello-world --help", expected: """ +Usage: hello-world [arguments...] [options...] [-h|--help] [--version] + +hello my world. + +Arguments: + [0] my boo is not boo. + +Options: + -f|-fb|--foo-bar my foo is not bar. (Required) + +"""); + } + + [Fact] + public void AppWithNoRootDefaultDisplaySubcommand() + { + var code = """ +var app = ConsoleApp.Create(); +app.Add(); +app.Add(); +app.Run(args); + +/// +/// My class +/// +[Command("mc")] +public class MyClass +{ + /// + /// hello my world. + /// + /// -b, my boo is not boo. + /// -f|-fb, my foo is not bar. + public void HelloWorld([Argument]int boo, string fooBar) + { + Console.Write("Hello World! " + fooBar); + } +} + +/// +/// My class 2 +/// +[Command("mc2")] +public class MyClass2 +{ + /// + /// hello my world. + /// + /// -b, my boo is not boo. + /// -f|-fb, my foo is not bar. + public void HelloWorld2([Argument]int boo, string fooBar) + { + Console.Write("Hello World2! " + fooBar); + } +} +"""; + verifier.Execute(code, args: "--help", expected: """ +Usage: [command] [-h|--help] [--version] + +Commands: + mc hello-world hello my world. + mc2 hello-world2 hello my world. + +"""); + + verifier.Execute(code, args: "mc hello-world --help", expected: """ +Usage: mc hello-world [arguments...] [options...] [-h|--help] [--version] + +hello my world. + +Arguments: + [0] my boo is not boo. + +Options: + -f|-fb|--foo-bar my foo is not bar. (Required) + +"""); + } + + [Fact] + public void AppWithClassRootWithHiddenSubcommands() + { + var code = """ +var app = ConsoleApp.Create(); +app.Add(); +app.Add(); +app.SubcommandHelp(ConsoleApp.DisplayType.Hidden); +app.Run(args); + +/// +/// My class +/// +[Command("mc")] +public class MyClass +{ + /// + /// hello my world. + /// + /// -b, my boo is not boo. + /// -f|-fb, my foo is not bar. + public void HelloWorld([Argument]int boo, string fooBar) + { + Console.Write("Hello World! " + fooBar); + } +} + +/// +/// My class 2 +/// +[Command("mc2")] +public class MyClass2 +{ + /// + /// hello my world. + /// + /// -b, my boo is not boo. + /// -f|-fb, my foo is not bar. + public void HelloWorld2([Argument]int boo, string fooBar) + { + Console.Write("Hello World2! " + fooBar); + } +} +"""; + verifier.Execute(code, args: "--help", expected: """ +Usage: [command] [options...] [-h|--help] [--version] + +Commands: + mc My class + mc2 My class 2 + +"""); + + verifier.Execute(code, args: "mc --help", expected: """ +Usage: mc [command] [options...] [-h|--help] [--version] + +My class + +Commands: + hello-world hello my world. + +"""); + + verifier.Execute(code, args: "mc hello-world --help", expected: """ +Usage: hello-world [arguments...] [options...] [-h|--help] [--version] + +hello my world. + +Arguments: + [0] my boo is not boo. + +Options: + -f|-fb|--foo-bar my foo is not bar. (Required) + +"""); + } + + [Fact] + public void AppWithClassRoot() + { + var code = """ +var app = ConsoleApp.Create(); +app.Add(); +app.Add(); +app.Run(args); + +/// +/// My class +/// +[Command("")] +public class Root +{ + /// + /// hello my world. + /// + /// -b, my boo is not boo. + /// -f|-fb, my foo is not bar. + public void HelloWorld([Argument]int boo, string fooBar) + { + Console.Write("Hello World! " + fooBar); + } +} + +/// +/// My class +/// +[Command("mc")] +public class MyClass +{ + /// + /// hello my world. + /// + /// -b, my boo is not boo. + /// -f|-fb, my foo is not bar. + public void HelloWorld2([Argument]int boo, string fooBar) + { + Console.Write("Hello World2! " + fooBar); + } +} +"""; + verifier.Execute(code, args: "--help", expected: """ +Usage: [command] [options...] [-h|--help] [--version] + +Commands: + hello-world My classz + mc hello-world hello my world. + +"""); + + verifier.Execute(code, args: "mc hello-world --help", expected: """ +Usage: hello-world [arguments...] [options...] [-h|--help] [--version] + +hello my world. + +Arguments: + [0] my boo is not boo. + +Options: + -f|-fb|--foo-bar my foo is not bar. (Required) + +"""); + } + + [Fact] + public void AppWithClassWithSameCommandName() + { + var code = """ +var app = ConsoleApp.Create(); +app.Add(); +app.Add(); +app.Run(args); + +/// +/// My class +/// +[Command("mc1")] +public class MyClass1 +{ + /// + /// hello my world. + /// + /// -b, my boo is not boo. + /// -f|-fb, my foo is not bar. + public void HelloWorld([Argument]int boo, string fooBar) + { + Console.Write("Hello World! " + fooBar); + } +} + +/// +/// My class +/// +[Command("mc2")] +public class MyClass2 +{ + /// + /// hello my world. + /// + /// -b, my boo is not boo. + /// -f|-fb, my foo is not bar. + public void HelloWorld([Argument]int boo, string fooBar) + { + Console.Write("Hello World2! " + fooBar); + } +} +"""; + verifier.Execute(code, args: "--help", expected: """ +Usage: [command] [-h|--help] [--version] + +Commands: + mc1 hello-world hello my world. + mc2 hello-world hello my world. + +"""); + + verifier.Execute(code, args: "mc1 hello-world --help", expected: """ +Usage: mc1 hello-world [arguments...] [options...] [-h|--help] [--version] + +hello my world. + +Arguments: + [0] my boo is not boo. + +Options: + -f|-fb|--foo-bar my foo is not bar. (Required) + +"""); + + verifier.Execute(code, args: "mc2 hello-world --help", expected: """ +Usage: mc2 hello-world [arguments...] [options...] [-h|--help] [--version] + +hello my world. + +Arguments: + [0] my boo is not boo. + +Options: + -f|-fb|--foo-bar my foo is not bar. (Required) + +"""); + } + + [Fact] + public void AppWithClassRootAndRootCommand() + { + var code = """ +var app = ConsoleApp.Create(); +app.Add(); +app.Add(); +app.Run(args); + +/// +/// My class +/// +[Command("")] +public class Root +{ + /// + /// hello my world. + /// + /// -b, my boo is not boo. + /// -f|-fb, my foo is not bar. + [Command("")] + public void HelloWorld([Argument]int boo, string fooBar) + { + Console.Write("Hello World! " + fooBar); + } +} + +/// +/// My class +/// +[Command("mc")] +public class MyClass +{ + /// + /// hello my world. + /// + /// -b, my boo is not boo. + /// -f|-fb, my foo is not bar. + public void HelloWorld2([Argument]int boo, string fooBar) + { + Console.Write("Hello World2! " + fooBar); + } +} +"""; + verifier.Execute(code, args: "--help", expected: """ +Usage: [command] | hello-world [arguments...] [options...] [-h|--help] [--version] + +Arguments: + [0] my boo is not boo. + +Options: + -f|-fb|--foo-bar my foo is not bar. (Required) + +Commands: + mc hello-world hello my world. + +"""); + + verifier.Execute(code, args: "mc hello-world --help", expected: """ +Usage: hello-world [arguments...] [options...] [-h|--help] [--version] + +hello my world. + +Arguments: + [0] my boo is not boo. + +Options: + -f|-fb|--foo-bar my foo is not bar. (Required) + +"""); + } + + [Fact] + public void AppWithClassRootAndRootCommandWithSameCommandName() + { + var code = """ +var app = ConsoleApp.Create(); +app.Add(); +app.Add(); +app.Run(args); + +/// +/// My class +/// +[Command("")] +public class Root +{ + /// + /// hello my world. + /// + /// -b, my boo is not boo. + /// -f|-fb, my foo is not bar. + [Command("")] + public void HelloWorld([Argument]int boo, string fooBar) + { + Console.Write("Hello World! " + fooBar); + } +} + +/// +/// My class +/// +[Command("mc")] +public class MyClass +{ + /// + /// hello my world. + /// + /// -b, my boo is not boo. + /// -f|-fb, my foo is not bar. + public void HelloWorld([Argument]int boo, string fooBar) + { + Console.Write("Hello World2! " + fooBar); + } +} +"""; + + verifier.Ok(code); + + verifier.Execute(code, args: "--help", expected: """ +Usage: [command] | hello-world [arguments...] [options...] [-h|--help] [--version] + +Arguments: + [0] my boo is not boo. + +Options: + -f|-fb|--foo-bar my foo is not bar. (Required) + +Commands: + mc hello-world hello my world. + +"""); + + verifier.Execute(code, args: "mc hello-world --help", expected: """ +Usage: hello-world [arguments...] [options...] [-h|--help] [--version] + +hello my world. + Arguments: [0] my boo is not boo. diff --git a/tests/ConsoleAppFramework.GeneratorTests/RunTest.cs b/tests/ConsoleAppFramework.GeneratorTests/RunTest.cs index 98b181ee..3043f6d3 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/RunTest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/RunTest.cs @@ -96,7 +96,7 @@ public class Obj public int Foo { get; set; } } } -""", "--foo 10 --bar aiueo --ft Grape --flag --half 1.3 --itt 99 --obj {\"Foo\":1999}", "10aiueoGrapeTrue1.3991999"); +""", "--foo 10 --bar aiueo --ft Grape --flag --half 1.3 --itt 99 --obj {\"Foo\":1999}", "10aiueoGrapeTrue13991999"); } [Fact]