diff --git a/CliFx.Analyzers.Tests/CliFx.Analyzers.Tests.csproj b/CliFx.Analyzers.Tests/CliFx.Analyzers.Tests.csproj index 2bdacc8..ac96222 100644 --- a/CliFx.Analyzers.Tests/CliFx.Analyzers.Tests.csproj +++ b/CliFx.Analyzers.Tests/CliFx.Analyzers.Tests.csproj @@ -2,7 +2,6 @@ net6.0 - true true opencover diff --git a/CliFx.Tests/CliFx.Tests.csproj b/CliFx.Tests/CliFx.Tests.csproj index 97bc38d..b548d34 100644 --- a/CliFx.Tests/CliFx.Tests.csproj +++ b/CliFx.Tests/CliFx.Tests.csproj @@ -2,7 +2,6 @@ net6.0 - true true opencover diff --git a/CliFx.Tests/OptionBindingSpecs.cs b/CliFx.Tests/OptionBindingSpecs.cs index 10155f5..3fbc891 100644 --- a/CliFx.Tests/OptionBindingSpecs.cs +++ b/CliFx.Tests/OptionBindingSpecs.cs @@ -496,6 +496,83 @@ public class Command : ICommand ); } + [Fact] + public async Task Option_binding_supports_multiple_inheritance_through_default_interface_members() + { + // Arrange + var commandType = DynamicCommandBuilder.Compile( + // language=cs + @" +public static class SharedContext +{ + public static int Foo { get; set; } + + public static bool Bar { get; set; } +} + +public interface IHasFoo : ICommand +{ + [CommandOption(""foo"")] + public int Foo + { + get => SharedContext.Foo; + set => SharedContext.Foo = value; + } +} + +public interface IHasBar : ICommand +{ + [CommandOption(""bar"")] + public bool Bar + { + get => SharedContext.Bar; + set => SharedContext.Bar = value; + } +} + +public interface IHasBaz : ICommand +{ + public string Baz { get; set; } +} + +[Command] +public class Command : IHasFoo, IHasBar, IHasBaz +{ + [CommandOption(""baz"")] + public string Baz { get; set; } + + public ValueTask ExecuteAsync(IConsole console) + { + console.Output.WriteLine(""Foo = "" + SharedContext.Foo); + console.Output.WriteLine(""Bar = "" + SharedContext.Bar); + console.Output.WriteLine(""Baz = "" + Baz); + + return default; + } +} +"); + + var application = new CliApplicationBuilder() + .AddCommand(commandType) + .UseConsole(FakeConsole) + .Build(); + + // Act + var exitCode = await application.RunAsync( + new[] { "--foo", "42", "--bar", "--baz", "xyz" } + ); + + var stdOut = FakeConsole.ReadOutputString(); + + // Assert + exitCode.Should().Be(0); + stdOut.Should().ConsistOfLines( + "Foo = 42", + "Bar = True", + "Baz = xyz" + ); + } + [Fact] public async Task Option_binding_does_not_consider_a_negative_number_as_an_option_name_or_short_name() { @@ -704,64 +781,4 @@ public class Command : ICommand exitCode.Should().NotBe(0); stdErr.Should().Contain("expects a single argument, but provided with multiple"); } - - - [Fact] - public async Task Option_bound_using_interfaces_for_multiple_inheritance_should_work() - { - // Arrange - var commandType = DynamicCommandBuilder.Compile( - // language=cs - @" -public static class FooBarLogger -{ - public static bool Foo { get; set; } = false; - public static bool Bar { get; set; } = false; -} -public interface IOptionBar : ICommand -{ - [CommandOption(""bar"")] - public bool Bar - { - get => FooBarLogger.Bar; - set => FooBarLogger.Bar = value; - } -} -public interface IOptionFoo : ICommand -{ - [CommandOption(""foo"")] - public bool Foo - { - get => FooBarLogger.Foo; - set => FooBarLogger.Foo = value; - } -} - -[Command] -public class Command : IOptionFoo, IOptionBar -{ - public ValueTask ExecuteAsync(IConsole console) - { - console.Output.WriteLine($""Foo: { FooBarLogger.Foo }""); - console.Output.WriteLine($""Bar: { FooBarLogger.Bar }""); - return default; - } -} -"); - - var application = new CliApplicationBuilder() - .AddCommand(commandType) - .UseConsole(FakeConsole) - .Build(); - - // Act - var exitCode = await application.RunAsync( - new[] {"--foo" , "--bar"}); - - var stdOut = FakeConsole.ReadOutputString(); - - // Assert - exitCode.Should().Be(0); - stdOut.Trim().Should().Be("Foo: True" + Environment.NewLine + "Bar: True"); - } } \ No newline at end of file diff --git a/CliFx/Schema/CommandSchema.cs b/CliFx/Schema/CommandSchema.cs index f6acb8c..97c87a3 100644 --- a/CliFx/Schema/CommandSchema.cs +++ b/CliFx/Schema/CommandSchema.cs @@ -86,12 +86,22 @@ internal partial class CommandSchema var implicitOptionSchemas = string.IsNullOrWhiteSpace(name) ? new[] {OptionSchema.HelpOption, OptionSchema.VersionOption} : new[] {OptionSchema.HelpOption}; - - // Include interface members for multiple inheritance - // If interface inherits from ICommand, it will be included - var interfaces = type.GetInterfaces().Where(i => i != typeof(ICommand) && typeof(ICommand).IsAssignableFrom(i) ); - var properties = type.GetProperties().Concat(interfaces.SelectMany(i => i.GetProperties())).ToArray(); - + + var properties = type + // Get properties directly on command type + .GetProperties() + // Get non-abstract properties on interfaces (to support default interfaces members) + .Union(type + .GetInterfaces() + // Only interfaces implementing ICommand for explicitness + .Where(i => typeof(ICommand).IsAssignableFrom(i) && i != typeof(ICommand)) + .SelectMany(i => i + .GetProperties() + .Where(p => !p.GetMethod.IsAbstract && !p.SetMethod.IsAbstract) + ) + ) + .ToArray(); + var parameterSchemas = properties .Select(ParameterSchema.TryResolve) .WhereNotNull()