diff --git a/CliFx.Analyzers/CommandSchemaAnalyzer.cs b/CliFx.Analyzers/CommandSchemaAnalyzer.cs index a688d34..cf652fb 100644 --- a/CliFx.Analyzers/CommandSchemaAnalyzer.cs +++ b/CliFx.Analyzers/CommandSchemaAnalyzer.cs @@ -275,11 +275,8 @@ namespace CliFx.Analyzers private static void CheckCommandType(SymbolAnalysisContext context) { // Named type: MyCommand - if (!(context.Symbol is INamedTypeSymbol namedTypeSymbol)) - return; - - // Only classes - if (namedTypeSymbol.TypeKind != TypeKind.Class) + if (!(context.Symbol is INamedTypeSymbol namedTypeSymbol) || + namedTypeSymbol.TypeKind != TypeKind.Class) return; // Implements ICommand? diff --git a/CliFx.Tests/ArgumentConversionSpecs.cs b/CliFx.Tests/ArgumentConversionSpecs.cs index 7be6731..097dc4c 100644 --- a/CliFx.Tests/ArgumentConversionSpecs.cs +++ b/CliFx.Tests/ArgumentConversionSpecs.cs @@ -913,7 +913,7 @@ namespace CliFx.Tests } [Fact] - public async Task Argument_value_can_be_bound_to_an_enum_type_by_name() + public async Task Argument_value_can_be_bound_to_enum_type_by_name() { // Arrange var (console, stdOut, _) = VirtualConsole.CreateBuffered(); @@ -941,7 +941,7 @@ namespace CliFx.Tests } [Fact] - public async Task Argument_value_can_be_bound_to_an_enum_type_by_id() + public async Task Argument_value_can_be_bound_to_enum_type_by_id() { // Arrange var (console, stdOut, _) = VirtualConsole.CreateBuffered(); @@ -1318,6 +1318,54 @@ namespace CliFx.Tests }); } + [Fact] + public async Task Argument_value_can_only_be_bound_if_the_target_type_is_supported() + { + // Arrange + var (console, _, stdErr) = VirtualConsole.CreateBuffered(); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--custom" + }); + + // Assert + exitCode.Should().NotBe(0); + stdErr.GetString().Should().NotBeNullOrWhiteSpace(); + + _output.WriteLine(stdErr.GetString()); + } + + [Fact] + public async Task Argument_value_can_only_be_bound_if_the_provided_value_can_be_converted_to_the_target_type() + { + // Arrange + var (console, _, stdErr) = VirtualConsole.CreateBuffered(); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--int", "foo" + }); + + // Assert + exitCode.Should().NotBe(0); + stdErr.GetString().Should().NotBeNullOrWhiteSpace(); + + _output.WriteLine(stdErr.GetString()); + } + [Fact] public async Task Argument_value_can_only_be_bound_to_non_nullable_type_if_it_is_set() { @@ -1365,5 +1413,29 @@ namespace CliFx.Tests _output.WriteLine(stdErr.GetString()); } + + [Fact] + public async Task Argument_values_can_only_be_bound_to_a_type_that_implements_IEnumerable_and_can_be_converted_from_an_array() + { + // Arrange + var (console, _, stdErr) = VirtualConsole.CreateBuffered(); + + var application = new CliApplicationBuilder() + .AddCommand() + .UseConsole(console) + .Build(); + + // Act + var exitCode = await application.RunAsync(new[] + { + "cmd", "--custom-enumerable" + }); + + // Assert + exitCode.Should().NotBe(0); + stdErr.GetString().Should().NotBeNullOrWhiteSpace(); + + _output.WriteLine(stdErr.GetString()); + } } } \ No newline at end of file diff --git a/CliFx.Tests/Commands/UnsupportedArgumentTypesCommand.cs b/CliFx.Tests/Commands/UnsupportedArgumentTypesCommand.cs index 4dc9648..f6efda9 100644 --- a/CliFx.Tests/Commands/UnsupportedArgumentTypesCommand.cs +++ b/CliFx.Tests/Commands/UnsupportedArgumentTypesCommand.cs @@ -8,11 +8,11 @@ namespace CliFx.Tests.Commands [Command("cmd")] public partial class UnsupportedArgumentTypesCommand : SelfSerializeCommandBase { - [CommandOption("str-non-initializable")] - public CustomType? StringNonInitializable { get; set; } + [CommandOption("custom")] + public CustomType? CustomNonConvertible { get; set; } - [CommandOption("str-enumerable-non-initializable")] - public CustomEnumerable? StringEnumerableNonInitializable { get; set; } + [CommandOption("custom-enumerable")] + public CustomEnumerable? CustomEnumerableNonConvertible { get; set; } } public partial class UnsupportedArgumentTypesCommand diff --git a/CliFx/CliApplicationBuilder.cs b/CliFx/CliApplicationBuilder.cs index dcce7e5..65e0e59 100644 --- a/CliFx/CliApplicationBuilder.cs +++ b/CliFx/CliApplicationBuilder.cs @@ -165,9 +165,9 @@ namespace CliFx /// public CliApplication Build() { - _title ??= TryGetDefaultTitle() ?? "App"; - _executableName ??= TryGetDefaultExecutableName() ?? "app"; - _versionText ??= TryGetDefaultVersionText() ?? "v1.0"; + _title ??= GetDefaultTitle(); + _executableName ??= GetDefaultExecutableName(); + _versionText ??= GetDefaultVersionText(); _console ??= new SystemConsole(); _typeActivator ??= new DefaultTypeActivator(); @@ -185,23 +185,29 @@ namespace CliFx // Entry assembly is null in tests private static Assembly? EntryAssembly => LazyEntryAssembly.Value; - private static string? TryGetDefaultTitle() => EntryAssembly?.GetName().Name; + private static string GetDefaultTitle() => EntryAssembly?.GetName().Name?? "App"; - private static string? TryGetDefaultExecutableName() + private static string GetDefaultExecutableName() { var entryAssemblyLocation = EntryAssembly?.Location; // The assembly can be an executable or a dll, depending on how it was packaged - var isDll = string.Equals(Path.GetExtension(entryAssemblyLocation), ".dll", StringComparison.OrdinalIgnoreCase); + var isDll = string.Equals( + Path.GetExtension(entryAssemblyLocation), + ".dll", + StringComparison.OrdinalIgnoreCase + ); - return isDll + var name = isDll ? "dotnet " + Path.GetFileName(entryAssemblyLocation) : Path.GetFileNameWithoutExtension(entryAssemblyLocation); + + return name ?? "app"; } - private static string? TryGetDefaultVersionText() => + private static string GetDefaultVersionText() => EntryAssembly != null ? $"v{EntryAssembly.GetName().Version.ToSemanticString()}" - : null; + : "v1.0"; } } \ No newline at end of file diff --git a/CliFx/DefaultTypeActivator.cs b/CliFx/DefaultTypeActivator.cs index dc945d2..5a6d27e 100644 --- a/CliFx/DefaultTypeActivator.cs +++ b/CliFx/DefaultTypeActivator.cs @@ -1,5 +1,6 @@ using System; using CliFx.Exceptions; +using CliFx.Internal.Extensions; namespace CliFx { @@ -13,7 +14,7 @@ namespace CliFx { try { - return Activator.CreateInstance(type); + return type.CreateInstance(); } catch (Exception ex) { diff --git a/CliFx/Internal/Extensions/TypeExtensions.cs b/CliFx/Internal/Extensions/TypeExtensions.cs index 42ac2f9..6a05cdb 100644 --- a/CliFx/Internal/Extensions/TypeExtensions.cs +++ b/CliFx/Internal/Extensions/TypeExtensions.cs @@ -8,7 +8,9 @@ namespace CliFx.Internal.Extensions { internal static class TypeExtensions { - public static T CreateInstance(this Type type) => (T) Activator.CreateInstance(type); + public static object CreateInstance(this Type type) => Activator.CreateInstance(type); + + public static T CreateInstance(this Type type) => (T) type.CreateInstance(); public static bool Implements(this Type type, Type interfaceType) => type.GetInterfaces().Contains(interfaceType);