diff --git a/CliFx.Tests/HelpTextSpecs.cs b/CliFx.Tests/HelpTextSpecs.cs index 7e5304d..9359ec8 100644 --- a/CliFx.Tests/HelpTextSpecs.cs +++ b/CliFx.Tests/HelpTextSpecs.cs @@ -223,56 +223,6 @@ public class NamedChildCommand : ICommand stdErr.Should().NotBeNullOrWhiteSpace(); } - // Regression test for #117 - [Fact] - public async Task Help_text_lists_parameters_in_specified_order() - { - // Arrange - var commandType = DynamicCommandBuilder.Compile( - // language=cs - @" -public abstract class CommandBase : ICommand -{ - [CommandParameter(0)] - public string Foo { get; set; } - - public abstract ValueTask ExecuteAsync(IConsole console); -} - -[Command] -public class Command : CommandBase -{ - [CommandParameter(1)] - public string Bar { get; set; } - - [CommandParameter(2)] - public IReadOnlyList Baz { get; set; } - - public override ValueTask ExecuteAsync(IConsole console) => default; -} -"); - - var application = new CliApplicationBuilder() - .AddCommand(commandType) - .UseConsole(FakeConsole) - .Build(); - - // Act - var exitCode = await application.RunAsync( - new[] { "--help" }, - new Dictionary() - ); - - var stdOut = FakeConsole.ReadOutputString(); - - // Assert - exitCode.Should().Be(0); - stdOut.Should().ContainAllInOrder( - "USAGE", - "", "", "" - ); - } - [Fact] public async Task Help_text_shows_application_metadata() { @@ -421,6 +371,57 @@ public class Command : ICommand ); } + // https://github.com/Tyrrrz/CliFx/issues/117 + [Fact] + public async Task Help_text_shows_usage_format_which_lists_all_parameters_in_specified_order() + { + // Arrange + var commandType = DynamicCommandBuilder.Compile( + // language=cs + @" +// Base members appear last in reflection order +public abstract class CommandBase : ICommand +{ + [CommandParameter(0)] + public string Foo { get; set; } + + public abstract ValueTask ExecuteAsync(IConsole console); +} + +[Command] +public class Command : CommandBase +{ + [CommandParameter(2)] + public IReadOnlyList Baz { get; set; } + + [CommandParameter(1)] + public string Bar { get; set; } + + public override ValueTask ExecuteAsync(IConsole console) => default; +} +"); + + var application = new CliApplicationBuilder() + .AddCommand(commandType) + .UseConsole(FakeConsole) + .Build(); + + // Act + var exitCode = await application.RunAsync( + new[] { "--help" }, + new Dictionary() + ); + + var stdOut = FakeConsole.ReadOutputString(); + + // Assert + exitCode.Should().Be(0); + stdOut.Should().ContainAllInOrder( + "USAGE", + "", "", "" + ); + } + [Fact] public async Task Help_text_shows_usage_format_which_lists_all_required_options() { diff --git a/CliFx/Extensibility/BindingValidator.cs b/CliFx/Extensibility/BindingValidator.cs index 08897a7..92a5e2c 100644 --- a/CliFx/Extensibility/BindingValidator.cs +++ b/CliFx/Extensibility/BindingValidator.cs @@ -29,7 +29,7 @@ public abstract class BindingValidator : IBindingValidator /// You can use the utility methods and to /// create an appropriate result. /// - public abstract BindingValidationError? Validate(T value); + public abstract BindingValidationError? Validate(T? value); - BindingValidationError? IBindingValidator.Validate(object? value) => Validate((T) value!); + BindingValidationError? IBindingValidator.Validate(object? value) => Validate((T?) value); } \ No newline at end of file diff --git a/CliFx/Formatting/HelpConsoleFormatter.cs b/CliFx/Formatting/HelpConsoleFormatter.cs index 97483dc..5e10e0d 100644 --- a/CliFx/Formatting/HelpConsoleFormatter.cs +++ b/CliFx/Formatting/HelpConsoleFormatter.cs @@ -72,7 +72,7 @@ internal class HelpConsoleFormatter : ConsoleFormatter Write(' '); // Parameters - foreach (var parameter in _context.CommandSchema.Parameters) + foreach (var parameter in _context.CommandSchema.Parameters.OrderBy(p => p.Order)) { Write(ConsoleColor.DarkCyan, parameter.Property.IsScalar() ? $"<{parameter.Name}>" diff --git a/CliFx/Schema/ApplicationSchema.cs b/CliFx/Schema/ApplicationSchema.cs index 445f840..308515e 100644 --- a/CliFx/Schema/ApplicationSchema.cs +++ b/CliFx/Schema/ApplicationSchema.cs @@ -16,8 +16,8 @@ internal partial class ApplicationSchema public IReadOnlyList GetCommandNames() => Commands .Select(c => c.Name) - .Where(n => !string.IsNullOrWhiteSpace(n)) - .ToArray()!; + .WhereNotNullOrWhiteSpace() + .ToArray(); public CommandSchema? TryFindDefaultCommand() => Commands.FirstOrDefault(c => c.IsDefault); diff --git a/CliFx/Schema/CommandSchema.cs b/CliFx/Schema/CommandSchema.cs index d71687e..cc75f7d 100644 --- a/CliFx/Schema/CommandSchema.cs +++ b/CliFx/Schema/CommandSchema.cs @@ -89,13 +89,12 @@ internal partial class CommandSchema var parameterSchemas = type.GetProperties() .Select(ParameterSchema.TryResolve) - .Where(p => p is not null) - .OrderBy(p => p!.Order) + .WhereNotNull() .ToArray(); var optionSchemas = type.GetProperties() .Select(OptionSchema.TryResolve) - .Where(o => o is not null) + .WhereNotNull() .Concat(implicitOptionSchemas) .ToArray(); @@ -103,8 +102,8 @@ internal partial class CommandSchema type, name, description, - parameterSchemas!, - optionSchemas! + parameterSchemas, + optionSchemas ); } diff --git a/CliFx/Utils/Extensions/CollectionExtensions.cs b/CliFx/Utils/Extensions/CollectionExtensions.cs index a2d7375..23dfcf1 100644 --- a/CliFx/Utils/Extensions/CollectionExtensions.cs +++ b/CliFx/Utils/Extensions/CollectionExtensions.cs @@ -6,6 +6,25 @@ namespace CliFx.Utils.Extensions; internal static class CollectionExtensions { + public static IEnumerable WhereNotNull(this IEnumerable source) + where T : class + { + foreach (var i in source) + { + if (i is not null) + yield return i; + } + } + + public static IEnumerable WhereNotNullOrWhiteSpace(this IEnumerable source) + { + foreach (var i in source) + { + if (!string.IsNullOrWhiteSpace(i)) + yield return i; + } + } + public static void RemoveRange(this ICollection source, IEnumerable items) { foreach (var item in items) @@ -17,5 +36,5 @@ internal static class CollectionExtensions IEqualityComparer comparer) => dictionary .Cast() - .ToDictionary(entry => (TKey) entry.Key, entry => (TValue) entry.Value, comparer)!; + .ToDictionary(entry => (TKey) entry.Key, entry => (TValue) entry.Value, comparer); } \ No newline at end of file