diff --git a/src/System.CommandLine.DragonFruit.Tests/System.CommandLine.DragonFruit.Tests.csproj b/src/System.CommandLine.DragonFruit.Tests/System.CommandLine.DragonFruit.Tests.csproj index 8994befbd7..23aa033d61 100644 --- a/src/System.CommandLine.DragonFruit.Tests/System.CommandLine.DragonFruit.Tests.csproj +++ b/src/System.CommandLine.DragonFruit.Tests/System.CommandLine.DragonFruit.Tests.csproj @@ -1,6 +1,6 @@  - net5.0 + net6.0 AutoGeneratedProgram true diff --git a/src/System.CommandLine.Generator.Tests/System.CommandLine.Generator.Tests.csproj b/src/System.CommandLine.Generator.Tests/System.CommandLine.Generator.Tests.csproj index 5b0e6d6aab..a423eb1e7a 100644 --- a/src/System.CommandLine.Generator.Tests/System.CommandLine.Generator.Tests.csproj +++ b/src/System.CommandLine.Generator.Tests/System.CommandLine.Generator.Tests.csproj @@ -1,6 +1,6 @@  - net5.0 + net6.0 $(TargetFrameworks);net462 true true diff --git a/src/System.CommandLine.Hosting.Tests/System.CommandLine.Hosting.Tests.csproj b/src/System.CommandLine.Hosting.Tests/System.CommandLine.Hosting.Tests.csproj index 09aa71c96b..328b4e1245 100644 --- a/src/System.CommandLine.Hosting.Tests/System.CommandLine.Hosting.Tests.csproj +++ b/src/System.CommandLine.Hosting.Tests/System.CommandLine.Hosting.Tests.csproj @@ -1,7 +1,7 @@  - net5.0 + net6.0 $(TargetFrameworks);net462 false diff --git a/src/System.CommandLine.NamingConventionBinder.Tests/System.CommandLine.NamingConventionBinder.Tests.csproj b/src/System.CommandLine.NamingConventionBinder.Tests/System.CommandLine.NamingConventionBinder.Tests.csproj index 008ac993db..8b6790f24b 100644 --- a/src/System.CommandLine.NamingConventionBinder.Tests/System.CommandLine.NamingConventionBinder.Tests.csproj +++ b/src/System.CommandLine.NamingConventionBinder.Tests/System.CommandLine.NamingConventionBinder.Tests.csproj @@ -1,6 +1,6 @@  - net5.0 + net6.0 $(TargetFrameworks);net462 10 diff --git a/src/System.CommandLine.Rendering.Tests/System.CommandLine.Rendering.Tests.csproj b/src/System.CommandLine.Rendering.Tests/System.CommandLine.Rendering.Tests.csproj index 7a2a5fecbf..1c116508e3 100644 --- a/src/System.CommandLine.Rendering.Tests/System.CommandLine.Rendering.Tests.csproj +++ b/src/System.CommandLine.Rendering.Tests/System.CommandLine.Rendering.Tests.csproj @@ -1,7 +1,7 @@ - net5.0 + net6.0 false diff --git a/src/System.CommandLine.Rendering.Tests/TableRenderingTests.cs b/src/System.CommandLine.Rendering.Tests/TableRenderingTests.cs index 7254579a5d..f8574deed5 100644 --- a/src/System.CommandLine.Rendering.Tests/TableRenderingTests.cs +++ b/src/System.CommandLine.Rendering.Tests/TableRenderingTests.cs @@ -8,7 +8,6 @@ using Xunit; using Xunit.Abstractions; using System.CommandLine.Rendering.Views; -using System.CommandLine.Tests; using System.CommandLine.Tests.Utility; using static System.Environment; diff --git a/src/System.CommandLine.Suggest.Tests/DotnetSuggestEndToEndTests.cs b/src/System.CommandLine.Suggest.Tests/DotnetSuggestEndToEndTests.cs index ef7fbfed17..ed7a07261c 100644 --- a/src/System.CommandLine.Suggest.Tests/DotnetSuggestEndToEndTests.cs +++ b/src/System.CommandLine.Suggest.Tests/DotnetSuggestEndToEndTests.cs @@ -6,9 +6,9 @@ using System.IO; using System.Linq; using System.Text; -using System.Threading.Tasks; using Xunit.Abstractions; using static System.Environment; +using Process = System.CommandLine.Tests.Utility.Process; namespace System.CommandLine.Suggest.Tests { @@ -72,11 +72,11 @@ private static void PrepareTestHomeDirectoryToAvoidPolluteBuildMachineHome() } [ReleaseBuildOnlyFact] - public async Task Test_app_supplies_suggestions() + public void Test_app_supplies_suggestions() { var stdOut = new StringBuilder(); - await ExecuteAsync( + Process.RunToCompletion( _endToEndTestApp.FullName, "[suggest:1] \"a\"", stdOut: value => stdOut.AppendLine(value), @@ -88,22 +88,22 @@ await ExecuteAsync( } [ReleaseBuildOnlyFact] - public async Task Dotnet_suggest_provides_suggestions_for_app() + public void Dotnet_suggest_provides_suggestions_for_app() { // run once to trigger a call to dotnet-suggest register - await ExecuteAsync( + Process.RunToCompletion( _endToEndTestApp.FullName, "-h", stdOut: s => _output.WriteLine(s), stdErr: s => _output.WriteLine(s), - environmentVariables: _environmentVariables); + environmentVariables: _environmentVariables).Should().Be(0); var stdOut = new StringBuilder(); var stdErr = new StringBuilder(); var commandLineToComplete = "a"; - await ExecuteAsync( + Process.RunToCompletion( _dotnetSuggest.FullName, $"get -e \"{_endToEndTestApp.FullName}\" --position {commandLineToComplete.Length} -- \"{commandLineToComplete}\"", stdOut: value => stdOut.AppendLine(value), @@ -123,22 +123,22 @@ await ExecuteAsync( } [ReleaseBuildOnlyFact] - public async Task Dotnet_suggest_provides_suggestions_for_app_with_only_commandname() + public void Dotnet_suggest_provides_suggestions_for_app_with_only_commandname() { // run once to trigger a call to dotnet-suggest register - await ExecuteAsync( + Process.RunToCompletion( _endToEndTestApp.FullName, "-h", stdOut: s => _output.WriteLine(s), stdErr: s => _output.WriteLine(s), - environmentVariables: _environmentVariables); + environmentVariables: _environmentVariables).Should().Be(0); var stdOut = new StringBuilder(); var stdErr = new StringBuilder(); var commandLineToComplete = "a "; - await ExecuteAsync( + Process.RunToCompletion( _dotnetSuggest.FullName, $"get -e \"{_endToEndTestApp.FullName}\" --position {commandLineToComplete.Length} -- \"{commandLineToComplete}\"", stdOut: value => stdOut.AppendLine(value), @@ -149,73 +149,12 @@ await ExecuteAsync( _output.WriteLine($"stdErr:{NewLine}{stdErr}{NewLine}"); stdErr.ToString() - .Should() - .BeEmpty(); + .Should() + .BeEmpty(); stdOut.ToString() - .Should() - .Be($"--apple{NewLine}--banana{NewLine}--cherry{NewLine}--durian{NewLine}--help{NewLine}--version{NewLine}-?{NewLine}-h{NewLine}/?{NewLine}/h{NewLine}"); - } - - private static async Task ExecuteAsync( - string command, - string args, - Action stdOut = null, - Action stdErr = null, - params (string key, string value)[] environmentVariables) - { - args ??= ""; - - var process = new Diagnostics.Process - { - StartInfo = - { - Arguments = args, - FileName = command, - RedirectStandardError = true, - RedirectStandardOutput = true, - RedirectStandardInput = true, - UseShellExecute = false - } - }; - - if (environmentVariables.Length > 0) - { - for (var i = 0; i < environmentVariables.Length; i++) - { - var (key, value) = environmentVariables[i]; - process.StartInfo.Environment.Add(key, value); - } - } - - if (stdOut != null) - { - process.OutputDataReceived += (sender, eventArgs) => - { - if (eventArgs.Data != null) - { - stdOut(eventArgs.Data); - } - }; - } - - if (stdErr != null) - { - process.ErrorDataReceived += (sender, eventArgs) => - { - if (eventArgs.Data != null) - { - stdErr(eventArgs.Data); - } - }; - } - - process.Start(); - - process.BeginOutputReadLine(); - process.BeginErrorReadLine(); - - await process.WaitForExitAsync(); + .Should() + .Be($"--apple{NewLine}--banana{NewLine}--cherry{NewLine}--durian{NewLine}--help{NewLine}--version{NewLine}-?{NewLine}-h{NewLine}/?{NewLine}/h{NewLine}"); } } } diff --git a/src/System.CommandLine.Suggest.Tests/EndToEndTestApp/EndToEndTestApp.csproj b/src/System.CommandLine.Suggest.Tests/EndToEndTestApp/EndToEndTestApp.csproj index f4d596fdf5..9a3eb54d35 100644 --- a/src/System.CommandLine.Suggest.Tests/EndToEndTestApp/EndToEndTestApp.csproj +++ b/src/System.CommandLine.Suggest.Tests/EndToEndTestApp/EndToEndTestApp.csproj @@ -6,7 +6,7 @@ Exe - net5.0 + net6.0 diff --git a/src/System.CommandLine.Suggest.Tests/dotnet-suggest.Tests.csproj b/src/System.CommandLine.Suggest.Tests/dotnet-suggest.Tests.csproj index 1eae028185..da6abedf0b 100644 --- a/src/System.CommandLine.Suggest.Tests/dotnet-suggest.Tests.csproj +++ b/src/System.CommandLine.Suggest.Tests/dotnet-suggest.Tests.csproj @@ -1,7 +1,7 @@ - net5.0 + net6.0 diff --git a/src/System.CommandLine.Suggest/DotnetMuxer.cs b/src/System.CommandLine.Suggest/DotnetMuxer.cs index c324413a04..92612e7044 100644 --- a/src/System.CommandLine.Suggest/DotnetMuxer.cs +++ b/src/System.CommandLine.Suggest/DotnetMuxer.cs @@ -1,54 +1,55 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +#if NET6_0_OR_GREATER + using System.IO; using System.Runtime.InteropServices; -namespace System.CommandLine.Suggest +namespace System.CommandLine.Suggest; + +internal static class DotnetMuxer { - internal static class DotnetMuxer + public static FileInfo Path { get; } + + static DotnetMuxer() { - public static FileInfo Path { get; } + var muxerFileName = ExecutableName("dotnet"); + var fxDepsFile = GetDataFromAppDomain("FX_DEPS_FILE"); - static DotnetMuxer() + if (string.IsNullOrEmpty(fxDepsFile)) { - var muxerFileName = ExecutableName("dotnet"); - var fxDepsFile = GetDataFromAppDomain("FX_DEPS_FILE"); - - if (string.IsNullOrEmpty(fxDepsFile)) - - { - return; - } - - var muxerDir = new FileInfo(fxDepsFile).Directory?.Parent?.Parent?.Parent; + return; + } - if (muxerDir == null) - { - return; + var muxerDir = new FileInfo(fxDepsFile).Directory?.Parent?.Parent?.Parent; - } + if (muxerDir is null) + { + return; + } - var muxerCandidate = new FileInfo(System.IO.Path.Combine(muxerDir.FullName, muxerFileName)); + var muxerCandidate = new FileInfo(System.IO.Path.Combine(muxerDir.FullName, muxerFileName)); - if (muxerCandidate.Exists) - { - Path = muxerCandidate; - } - else - { - throw new InvalidOperationException("no muxer!"); - } + if (muxerCandidate.Exists) + { + Path = muxerCandidate; } - - public static string GetDataFromAppDomain(string propertyName) + else { - return AppContext.GetData(propertyName) as string; + throw new InvalidOperationException("no muxer!"); } + } - public static string ExecutableName(this string withoutExtension) => - RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? withoutExtension + ".exe" - : withoutExtension; + public static string GetDataFromAppDomain(string propertyName) + { + return AppContext.GetData(propertyName) as string; } + + public static string ExecutableName(this string withoutExtension) => + RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? withoutExtension + ".exe" + : withoutExtension; } + +#endif \ No newline at end of file diff --git a/src/System.CommandLine.Suggest/dotnet-suggest.csproj b/src/System.CommandLine.Suggest/dotnet-suggest.csproj index 8c2737f97a..ee994e3f6f 100644 --- a/src/System.CommandLine.Suggest/dotnet-suggest.csproj +++ b/src/System.CommandLine.Suggest/dotnet-suggest.csproj @@ -1,7 +1,7 @@  Exe - net5.0 + net6.0 true true dotnet-suggest diff --git a/src/System.CommandLine.Tests/Binding/TypeConversionTests.cs b/src/System.CommandLine.Tests/Binding/TypeConversionTests.cs index a0b9b6af1a..27514b2c82 100644 --- a/src/System.CommandLine.Tests/Binding/TypeConversionTests.cs +++ b/src/System.CommandLine.Tests/Binding/TypeConversionTests.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Collections; using System.Collections.Generic; using System.IO; using FluentAssertions; @@ -12,7 +13,7 @@ namespace System.CommandLine.Tests.Binding public class TypeConversionTests { [Fact] - public void Option_argument_with_arity_of_one_can_be_bound_without_custom_conversion_logic_if_the_type_has_a_constructor_that_takes_a_single_string() + public void Option_argument_of_FileInfo_can_be_bound_without_custom_conversion_logic() { var option = new Option("--file"); @@ -26,7 +27,7 @@ public void Option_argument_with_arity_of_one_can_be_bound_without_custom_conver } [Fact] - public void Command_argument_with_arity_of_one_can_be_bound_without_custom_conversion_logic_if_the_type_has_a_constructor_that_takes_a_single_string() + public void Command_argument_of_FileInfo_can_be_bound_without_custom_conversion_logic() { var argument = new Argument("the-arg"); @@ -45,7 +46,7 @@ public void Command_argument_with_arity_of_one_can_be_bound_without_custom_conve } [Fact] - public void Command_argument_with_arity_of_zero_or_one_when_type_has_a_constructor_that_takes_a_single_string_returns_null_when_argument_is_not_provided() + public void Command_argument_of_FileInfo_returns_null_when_argument_is_not_provided() { var argument = new Argument("the-arg") { @@ -64,7 +65,7 @@ public void Command_argument_with_arity_of_zero_or_one_when_type_has_a_construct } [Fact] - public void Argument_with_arity_of_many_can_be_called_without_custom_conversion_logic_if_the_item_type_has_a_constructor_that_takes_a_single_string() + public void Argument_of_array_of_FileInfo_can_be_called_without_custom_conversion_logic() { var option = new Option("--file"); @@ -162,7 +163,7 @@ public void Argument_does_not_parse_as_the_default_value_when_the_option_has_bee [InlineData("the-command -x true")] [InlineData("the-command -x:true")] [InlineData("the-command -x=true")] - public void Bool_does_not_parse_as_the_default_value_when_the_option_has_been_applied(string commandLine) + public void Bool_parses_as_true_when_the_option_has_been_applied(string commandLine) { var option = new Option("-x"); @@ -175,7 +176,40 @@ public void Bool_does_not_parse_as_the_default_value_when_the_option_has_been_ap .Parse(commandLine) .GetValueForOption(option) .Should() - .Be(true); + .BeTrue(); + } + + [Theory] + [InlineData("the-command -x")] + [InlineData("the-command -x true")] + [InlineData("the-command -x:true")] + [InlineData("the-command -x=true")] + public void Nullable_bool_parses_as_true_when_the_option_has_been_applied(string commandLine) + { + var option = new Option("-x"); + + var command = new Command("the-command") + { + option + }; + + command + .Parse(commandLine) + .GetValueForOption(option) + .Should() + .BeTrue(); + } + + [Fact] + public void Nullable_bool_parses_as_null_when_the_option_has_not_been_applied() + { + var option = new Option("-x"); + + option + .Parse("") + .GetValueForOption(option) + .Should() + .Be(null); } [Fact] @@ -493,6 +527,16 @@ public void Specifying_an_option_argument_overrides_the_default_value() value.Should().Be(456); } + [Fact] + public void Values_can_be_correctly_converted_to_decimal_without_the_parser_specifying_a_custom_converter() + { + var option = new Option("-x", arity: ArgumentArity.ZeroOrOne); + + var value = option.Parse("-x 123.456").GetValueForOption(option); + + value.Should().Be(123.456m); + } + [Fact] public void Values_can_be_correctly_converted_to_double_without_the_parser_specifying_a_custom_converter() { @@ -504,13 +548,13 @@ public void Values_can_be_correctly_converted_to_double_without_the_parser_speci } [Fact] - public void Values_can_be_correctly_converted_to_float_without_the_parser_specifying_a_custom_converter() + public void Values_can_be_correctly_converted_to_Uri_without_the_parser_specifying_a_custom_converter() { var option = new Option("-x", arity: ArgumentArity.ZeroOrOne); - var value = option.Parse("-x 123.456").GetValueForOption(option); + var value = option.Parse("-x http://example.com").GetValueForOption(option); - value.Should().Be(123.456f); + value.Should().BeEquivalentTo(new Uri("http://example.com")); } [Fact] @@ -520,7 +564,7 @@ public void Options_with_no_arguments_specified_can_be_correctly_converted_to_bo option.Parse("-x").GetValueForOption(option).Should().BeTrue(); } - + [Fact] public void Options_with_arguments_specified_can_be_correctly_converted_to_bool_without_the_parser_specifying_a_custom_converter() { @@ -658,5 +702,55 @@ public void When_getting_an_array_of_values_and_specifying_a_conversion_type_tha .Should() .Be("Cannot parse argument 'not-an-int' for option '-x' as expected type 'System.Int32'."); } + + [Fact] + public void String_defaults_to_null_when_not_specified() + { + var argument = new Argument(); + var command = new Command("mycommand") + { + argument + }; + + var result = command.Parse("mycommand"); + result.GetValueForArgument(argument) + .Should() + .BeNull(); + } + + [Theory] + [InlineData(typeof(List))] + [InlineData(typeof(List))] + [InlineData(typeof(List))] + [InlineData(typeof(IEnumerable))] + [InlineData(typeof(IEnumerable))] + [InlineData(typeof(IEnumerable))] + [InlineData(typeof(ICollection))] + [InlineData(typeof(ICollection))] + [InlineData(typeof(ICollection))] + [InlineData(typeof(IList))] + [InlineData(typeof(IList))] + [InlineData(typeof(IList))] + [InlineData(typeof(string[]))] + [InlineData(typeof(int[]))] + [InlineData(typeof(FileAccess[]))] + [InlineData(typeof(IEnumerable))] + [InlineData(typeof(ICollection))] + [InlineData(typeof(IList))] + public void Sequence_type_defaults_to_empty_when_not_specified(Type sequenceType) + { + var argument = Activator.CreateInstance(typeof(Argument<>).MakeGenericType(sequenceType)); + + AssertParsedValueIsEmpty((dynamic)argument); + } + + private void AssertParsedValueIsEmpty(Argument argument) where T : IEnumerable + { + var result = argument.Parse(""); + + result.GetValueForArgument(argument) + .Should() + .BeEmpty(); + } } } \ No newline at end of file diff --git a/src/System.CommandLine.Tests/CommandTests.cs b/src/System.CommandLine.Tests/CommandTests.cs index 42c8806809..9fc6f684ac 100644 --- a/src/System.CommandLine.Tests/CommandTests.cs +++ b/src/System.CommandLine.Tests/CommandTests.cs @@ -276,148 +276,6 @@ public void When_Name_is_set_to_its_current_value_then_it_is_not_removed_from_al command.Aliases.Should().Contain("name"); } - [Fact] - public void Command_argument_of_string_defaults_to_empty_when_not_specified() - { - var argument = new Argument(); - var command = new Command("mycommand") - { - argument - }; - - var result = command.Parse("mycommand"); - result.GetValueForArgument(argument) - .Should() - .BeNull(); - } - - [Fact] - public void Command_argument_of_IEnumerable_of_T_defaults_to_empty_when_not_specified() - { - var argument = new Argument>(); - var command = new Command("mycommand") - { - argument - }; - - var result = command.Parse("mycommand"); - result.GetValueForArgument(argument) - .Should() - .BeEmpty(); - } - - [Fact] - public void Command_argument_of_Array_defaults_to_empty_when_not_specified() - { - var argument = new Argument(); - var command = new Command("mycommand") - { - argument - }; - - var result = command.Parse("mycommand"); - - result.GetValueForArgument(argument) - .Should() - .BeEmpty(); - } - - [Fact] - public void Command_argument_of_List_defaults_to_empty_when_not_specified() - { - var argument = new Argument>(); - var command = new Command("mycommand") - { - argument - }; - - var result = command.Parse("mycommand"); - - result.GetValueForArgument(argument) - .Should() - .BeEmpty(); - } - - [Fact] - public void Command_argument_of_IList_of_T_defaults_to_empty_when_not_specified() - { - var argument = new Argument>(); - var command = new Command("mycommand") - { - argument - }; - - var result = command.Parse("mycommand"); - - result.GetValueForArgument(argument) - .Should() - .BeEmpty(); - } - - [Fact] - public void Command_argument_of_IList_defaults_to_empty_when_not_specified() - { - var argument = new Argument(); - var command = new Command("mycommand") - { - argument - }; - - var result = command.Parse("mycommand"); - - result.GetValueForArgument(argument) - .Should() - .BeEmpty(); - } - - [Fact] - public void Command_argument_of_ICollection_defaults_to_empty_when_not_specified() - { - var argument = new Argument(); - var command = new Command("mycommand") - { - argument - }; - - var result = command.Parse("mycommand"); - - result.GetValueForArgument(argument) - .Should() - .BeEmpty(); - } - - [Fact] - public void Command_argument_of_IEnumerable_defaults_to_empty_when_not_specified() - { - var argument = new Argument(); - var command = new Command("mycommand") - { - argument - }; - - var result = command.Parse("mycommand"); - - result.GetValueForArgument(argument) - .Should() - .BeEmpty(); - } - - [Fact] - public void Command_argument_of_ICollection_of_T_defaults_to_empty_when_not_specified() - { - var argument = new Argument>(); - var command = new Command("mycommand") - { - argument - }; - - var result = command.Parse("mycommand"); - - result.GetValueForArgument(argument) - .Should() - .BeEmpty(); - } - [Fact] public void AddGlobalOption_updates_Options_property() { @@ -426,8 +284,8 @@ public void AddGlobalOption_updates_Options_property() command.AddGlobalOption(option); command.Options - .Should() - .Contain(option); + .Should() + .Contain(option); } // https://github.com/dotnet/command-line-api/issues/1437 diff --git a/src/System.CommandLine.Tests/Invocation/CancelOnProcessTerminationTests.cs b/src/System.CommandLine.Tests/Invocation/CancelOnProcessTerminationTests.cs index ef61bb2541..cac6f5fb92 100644 --- a/src/System.CommandLine.Tests/Invocation/CancelOnProcessTerminationTests.cs +++ b/src/System.CommandLine.Tests/Invocation/CancelOnProcessTerminationTests.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using FluentAssertions; using Xunit; +using Process = System.Diagnostics.Process; namespace System.CommandLine.Tests.Invocation { diff --git a/src/System.CommandLine.Tests/OptionTests.cs b/src/System.CommandLine.Tests/OptionTests.cs index 74bcb43ab7..626b93e897 100644 --- a/src/System.CommandLine.Tests/OptionTests.cs +++ b/src/System.CommandLine.Tests/OptionTests.cs @@ -332,116 +332,7 @@ public void Option_of_string_defaults_to_null_when_not_specified() .Should() .BeNull(); } - - [Fact] - public void Option_of_IEnumerable_of_T_defaults_to_empty_when_not_specified() - { - var option = new Option>("-x"); - - var result = option.Parse(""); - result.HasOption(option) - .Should() - .BeFalse(); - result.GetValueForOption(option) - .Should() - .BeEmpty(); - } - - [Fact] - public void Option_of_Array_defaults_to_empty_when_not_specified() - { - var option = new Option("-x"); - - var result = option.Parse(""); - result.HasOption(option) - .Should() - .BeFalse(); - result.GetValueForOption(option) - .Should() - .BeEmpty(); - } - - [Fact] - public void Option_of_List_defaults_to_empty_when_not_specified() - { - var option = new Option>("-x"); - - var result = option.Parse(""); - result.HasOption(option) - .Should() - .BeFalse(); - result.GetValueForOption(option) - .Should() - .BeEmpty(); - } - - [Fact] - public void Option_of_IList_of_T_defaults_to_empty_when_not_specified() - { - var option = new Option>("-x"); - - var result = option.Parse(""); - result.HasOption(option) - .Should() - .BeFalse(); - result.GetValueForOption(option) - .Should() - .BeEmpty(); - } - - [Fact] - public void Option_of_IList_defaults_to_empty_when_not_specified() - { - var option = new Option("-x"); - var result = option.Parse(""); - result.HasOption(option) - .Should() - .BeFalse(); - result.GetValueForOption(option) - .Should() - .BeEmpty(); - } - - [Fact] - public void Option_of_ICollection_defaults_to_empty_when_not_specified() - { - var option = new Option("-x"); - var result = option.Parse(""); - result.HasOption(option) - .Should() - .BeFalse(); - result.GetValueForOption(option) - .Should() - .BeEmpty(); - } - - [Fact] - public void Option_of_IEnumerable_defaults_to_empty_when_not_specified() - { - var option = new Option("-x"); - var result = option.Parse(""); - result.HasOption(option) - .Should() - .BeFalse(); - result.GetValueForOption(option) - .Should() - .BeEmpty(); - } - - [Fact] - public void Option_of_ICollection_of_T_defaults_to_empty_when_not_specified() - { - var option = new Option>("-x"); - - var result = option.Parse(""); - result.HasOption(option) - .Should() - .BeFalse(); - result.GetValueForOption(option) - .Should() - .BeEmpty(); - } - + [Fact] public void When_Name_is_set_to_its_current_value_then_it_is_not_removed_from_aliases() { diff --git a/src/System.CommandLine.Tests/ParserTests.cs b/src/System.CommandLine.Tests/ParserTests.cs index 79a7bd9cac..40240e630f 100644 --- a/src/System.CommandLine.Tests/ParserTests.cs +++ b/src/System.CommandLine.Tests/ParserTests.cs @@ -1593,7 +1593,7 @@ public void When_option_arguments_are_greater_than_maximum_arity_then_an_error_i } - [Fact] + [Fact(Skip = "Can we support both type converters and trimming?")] public void Argument_with_custom_type_converter_can_be_bound() { var option = new Option("--value"); @@ -1605,7 +1605,7 @@ public void Argument_with_custom_type_converter_can_be_bound() instance.Values.Should().BeEquivalentTo("a", "b", "c"); } - [Fact] + [Fact(Skip = "Can we support both type converters and trimming?")] public void Argument_with_custom_collection_type_converter_can_be_bound() { var option = new Option("--value") { Arity = ArgumentArity.ExactlyOne }; diff --git a/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj b/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj index 99b5483f33..6b13c9bea9 100644 --- a/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj +++ b/src/System.CommandLine.Tests/System.CommandLine.Tests.csproj @@ -1,6 +1,6 @@  - net5.0 + net6.0 $(TargetFrameworks);net462 false @@ -13,6 +13,24 @@ + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + + + diff --git a/src/System.CommandLine.Tests/TrimmingTestApp/Program.cs b/src/System.CommandLine.Tests/TrimmingTestApp/Program.cs new file mode 100644 index 0000000000..dab823ff86 --- /dev/null +++ b/src/System.CommandLine.Tests/TrimmingTestApp/Program.cs @@ -0,0 +1,16 @@ +using System.CommandLine; +using System.CommandLine.Invocation; + +var fileOption = new Argument().LegalFileNamesOnly(); + +var command = new RootCommand +{ + fileOption +}; + +command.SetHandler((FileInfo file, InvocationContext ctx) => +{ + ctx.Console.Write($"The file you chose was: {file}"); +}, fileOption); + +command.Invoke(args); diff --git a/src/System.CommandLine.Tests/TrimmingTestApp/TrimmingTestApp.csproj b/src/System.CommandLine.Tests/TrimmingTestApp/TrimmingTestApp.csproj new file mode 100644 index 0000000000..f113531b26 --- /dev/null +++ b/src/System.CommandLine.Tests/TrimmingTestApp/TrimmingTestApp.csproj @@ -0,0 +1,23 @@ + + + + Exe + net6.0 + enable + enable + true + false + + + + ..\..\System.CommandLine\bin\Release\net6.0\System.CommandLine.dll + + + + + $(SystemCommandLineDllPath) + + + + + \ No newline at end of file diff --git a/src/System.CommandLine.Tests/TrimmingTests.cs b/src/System.CommandLine.Tests/TrimmingTests.cs new file mode 100644 index 0000000000..439cead99f --- /dev/null +++ b/src/System.CommandLine.Tests/TrimmingTests.cs @@ -0,0 +1,54 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#if NET6_0_OR_GREATER + +using System.CommandLine.Suggest; +using System.CommandLine.Tests.Utility; +using System.IO; +using System.Text; +using FluentAssertions; +using Xunit.Abstractions; + +namespace System.CommandLine.Tests; + +public class TrimmingTests +{ + private readonly ITestOutputHelper _output; + private readonly string _systemCommandLineDllPath; + + public TrimmingTests(ITestOutputHelper output) + { + _output = output; + + _systemCommandLineDllPath = typeof(Command).Assembly.Location; + } + + [ReleaseBuildOnlyFact] + public void App_referencing_system_commandline_can_be_trimmed() + { + var stdOut = new StringBuilder(); + var stdErr = new StringBuilder(); + + var exitCode = Process.RunToCompletion( + DotnetMuxer.Path.FullName, + $"publish -c Release -r win-x64 --self-contained /p:PublishTrimmed=true /p:SystemCommandLineDllPath=\"{_systemCommandLineDllPath}\" /p:TreatWarningsAsErrors=true", + s => + { + _output.WriteLine(s); + stdOut.Append(s); + }, + s => + { + _output.WriteLine(s); + stdErr.Append(s); + }, + workingDirectory: Path.Combine(Directory.GetCurrentDirectory(), "TrimmingTestApp")); + + stdOut.ToString().Should().NotContain("warning IL"); + stdErr.ToString().Should().BeEmpty(); + exitCode.Should().Be(0); + } +} + +#endif \ No newline at end of file diff --git a/src/System.CommandLine.Tests/Utility/Process.cs b/src/System.CommandLine.Tests/Utility/Process.cs new file mode 100644 index 0000000000..db7f68aa26 --- /dev/null +++ b/src/System.CommandLine.Tests/Utility/Process.cs @@ -0,0 +1,76 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace System.CommandLine.Tests.Utility; + +public static class Process +{ + public static int RunToCompletion( + string command, + string args, + Action stdOut = null, + Action stdErr = null, + string workingDirectory = null, + params (string key, string value)[] environmentVariables) + { + args ??= ""; + + var process = new Diagnostics.Process + { + StartInfo = + { + Arguments = args, + FileName = command, + RedirectStandardError = true, + RedirectStandardOutput = true, + RedirectStandardInput = true, + UseShellExecute = false + } + }; + + if (!string.IsNullOrWhiteSpace(workingDirectory)) + { + process.StartInfo.WorkingDirectory = workingDirectory; + } + + if (environmentVariables.Length > 0) + { + for (var i = 0; i < environmentVariables.Length; i++) + { + var (key, value) = environmentVariables[i]; + process.StartInfo.Environment.Add(key, value); + } + } + + if (stdOut != null) + { + process.OutputDataReceived += (sender, eventArgs) => + { + if (eventArgs.Data != null) + { + stdOut(eventArgs.Data); + } + }; + } + + if (stdErr != null) + { + process.ErrorDataReceived += (sender, eventArgs) => + { + if (eventArgs.Data != null) + { + stdErr(eventArgs.Data); + } + }; + } + + process.Start(); + + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + process.WaitForExit(); + + return process.ExitCode; + } +} \ No newline at end of file diff --git a/src/System.CommandLine.Tests/Utility/RemoteExecution.cs b/src/System.CommandLine.Tests/Utility/RemoteExecution.cs index 20ab09b8ee..9f51da257a 100644 --- a/src/System.CommandLine.Tests/Utility/RemoteExecution.cs +++ b/src/System.CommandLine.Tests/Utility/RemoteExecution.cs @@ -12,7 +12,7 @@ public class RemoteExecution : IDisposable private const int FailWaitTimeoutMilliseconds = 60 * 1000; private readonly string _exceptionFile; - public RemoteExecution(Process process, string className, string methodName, string exceptionFile) + public RemoteExecution(Diagnostics.Process process, string className, string methodName, string exceptionFile) { Process = process; ClassName = className; @@ -20,7 +20,7 @@ public RemoteExecution(Process process, string className, string methodName, str _exceptionFile = exceptionFile; } - public Process Process { get; private set; } + public Diagnostics.Process Process { get; private set; } public string ClassName { get; } public string MethodName { get; } diff --git a/src/System.CommandLine.Tests/Utility/RemoteExecutor.cs b/src/System.CommandLine.Tests/Utility/RemoteExecutor.cs index 92870a43f7..23f7500f27 100644 --- a/src/System.CommandLine.Tests/Utility/RemoteExecutor.cs +++ b/src/System.CommandLine.Tests/Utility/RemoteExecutor.cs @@ -96,7 +96,7 @@ private static RemoteExecution Execute(MethodInfo methodInfo, string[] args, Pro string methodName = methodInfo.Name; string exceptionFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - string dotnetExecutable = Process.GetCurrentProcess().MainModule.FileName; + string dotnetExecutable = Diagnostics.Process.GetCurrentProcess().MainModule.FileName; string thisAssembly = typeof(RemoteExecutor).Assembly.Location; var assembly = (Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()); string entryAssemblyWithoutExtension = Path.Combine(Path.GetDirectoryName(assembly.Location), @@ -127,7 +127,7 @@ private static RemoteExecution Execute(MethodInfo methodInfo, string[] args, Pro } psi.Arguments = string.Join(" ", argumentList); - Process process = Process.Start(psi); + Diagnostics.Process process = Diagnostics.Process.Start(psi); return new RemoteExecution(process, className, methodName, exceptionFile); } @@ -159,7 +159,7 @@ private static string[] GetApplicationArguments() if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - s_arguments = File.ReadAllText($"/proc/{Process.GetCurrentProcess().Id}/cmdline").Split(new[] { '\0' }); + s_arguments = File.ReadAllText($"/proc/{Diagnostics.Process.GetCurrentProcess().Id}/cmdline").Split(new[] { '\0' }); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { diff --git a/src/System.CommandLine/Argument.cs b/src/System.CommandLine/Argument.cs index 9d6553c39d..5d9c0d280d 100644 --- a/src/System.CommandLine/Argument.cs +++ b/src/System.CommandLine/Argument.cs @@ -17,7 +17,9 @@ public class Argument : Symbol, IValueDescriptor private Func? _defaultValueFactory; private ArgumentArity _arity; private TryConvertArgument? _convertArguments; + private Type _valueType = typeof(string); + private CompletionSourceList? _completions = null; private List>? _validators = null; @@ -87,6 +89,7 @@ internal TryConvertArgument? ConvertArguments public virtual Type ValueType { get => _valueType; + set => _valueType = value ?? throw new ArgumentNullException(nameof(value)); } diff --git a/src/System.CommandLine/ArgumentArity.cs b/src/System.CommandLine/ArgumentArity.cs index c95776cfba..f887c474c1 100644 --- a/src/System.CommandLine/ArgumentArity.cs +++ b/src/System.CommandLine/ArgumentArity.cs @@ -66,7 +66,7 @@ public bool Equals(ArgumentArity other) => other.IsNonDefault == IsNonDefault; /// - public override bool Equals(object obj) => obj is ArgumentArity arity && Equals(arity); + public override bool Equals(object? obj) => obj is ArgumentArity arity && Equals(arity); /// public override int GetHashCode() @@ -117,31 +117,31 @@ public override int GetHashCode() /// /// An arity that does not allow any values. /// - public static ArgumentArity Zero => new ArgumentArity(0, 0); + public static ArgumentArity Zero => new(0, 0); /// /// An arity that may have one value, but no more than one. /// - public static ArgumentArity ZeroOrOne => new ArgumentArity(0, 1); + public static ArgumentArity ZeroOrOne => new(0, 1); /// /// An arity that must have exactly one value. /// - public static ArgumentArity ExactlyOne => new ArgumentArity(1, 1); + public static ArgumentArity ExactlyOne => new(1, 1); /// /// An arity that may have multiple values. /// - public static ArgumentArity ZeroOrMore => new ArgumentArity(0, MaximumArity); + public static ArgumentArity ZeroOrMore => new(0, MaximumArity); /// /// An arity that must have at least one value. /// - public static ArgumentArity OneOrMore => new ArgumentArity(1, MaximumArity); + public static ArgumentArity OneOrMore => new(1, MaximumArity); internal static ArgumentArity Default(Type type, Argument argument, ParentNode? firstParent) { - if (type == typeof(bool)) + if (type == typeof(bool) || type == typeof(bool?)) { return ZeroOrOne; } diff --git a/src/System.CommandLine/Binding/ArgumentConverter.DefaultValues.cs b/src/System.CommandLine/Binding/ArgumentConverter.DefaultValues.cs new file mode 100644 index 0000000000..2538a16a3d --- /dev/null +++ b/src/System.CommandLine/Binding/ArgumentConverter.DefaultValues.cs @@ -0,0 +1,72 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using System.Runtime.Serialization; + +namespace System.CommandLine.Binding; + +internal static partial class ArgumentConverter +{ +#if NET6_0_OR_GREATER + private static readonly Lazy _listCtor = + new(() => typeof(List<>) + .GetConstructors() + .SingleOrDefault(c => c.GetParameters().Length == 0)!); +#endif + + private static Array CreateEmptyArray(Type itemType, int capacity = 0) + => Array.CreateInstance(itemType, capacity); + + private static IList CreateEmptyList(Type listType) + { +#if NET6_0_OR_GREATER + var ctor = (ConstructorInfo)listType.GetMemberWithSameMetadataDefinitionAs(_listCtor.Value); +#else + var ctor = listType + .GetConstructors() + .SingleOrDefault(c => c.GetParameters().Length == 0); +#endif + + return (IList)ctor.Invoke(new object[] { }); + } + + private static IList CreateEnumerable(Type type, Type itemType, int capacity = 0) + { + if (type.IsArray) + { + return CreateEmptyArray(itemType, capacity); + } + + if (type.IsGenericType) + { + var x = type.GetGenericTypeDefinition() switch + { + { } enumerable when typeof(IEnumerable<>).IsAssignableFrom(enumerable) => + CreateEmptyArray(itemType, capacity), + { } array when typeof(IList<>).IsAssignableFrom(array) || + typeof(ICollection<>).IsAssignableFrom(array) => + CreateEmptyArray(itemType, capacity), + { } list when list == typeof(List<>) => + CreateEmptyList(type), + _ => null + }; + + if (x is { }) + { + return x; + } + } + + throw new ArgumentException($"Type {type} cannot be created without a custom binder."); + } + + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2067:UnrecognizedReflectionPattern", + Justification = $"{nameof(CreateDefaultValueType)} is only called on a ValueType. You can always create an instance of a ValueType.")] + private static object CreateDefaultValueType(Type type) => + FormatterServices.GetUninitializedObject(type); +} \ No newline at end of file diff --git a/src/System.CommandLine/Binding/ArgumentConverter.StringConverters.cs b/src/System.CommandLine/Binding/ArgumentConverter.StringConverters.cs new file mode 100644 index 0000000000..f813403ea5 --- /dev/null +++ b/src/System.CommandLine/Binding/ArgumentConverter.StringConverters.cs @@ -0,0 +1,146 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using System.IO; + +namespace System.CommandLine.Binding; + +internal static partial class ArgumentConverter +{ + private static readonly Dictionary _stringConverters = new() + { + [typeof(bool)] = (string token, out object? value) => + { + if (bool.TryParse(token, out var parsed)) + { + value = parsed; + return true; + } + + value = default; + return false; + }, + + [typeof(bool?)] = (string token, out object? value) => + { + if (bool.TryParse(token, out var parsed)) + { + value = parsed; + return true; + } + + value = default; + return false; + }, + + [typeof(decimal)] = (string input, out object? value) => + { + if (decimal.TryParse(input, out var parsed)) + { + value = parsed; + return true; + } + + value = default; + return false; + }, + + [typeof(DirectoryInfo)] = (string path, out object? value) => + { + value = new DirectoryInfo(path); + return true; + }, + + [typeof(double)] = (string input, out object? value) => + { + if (double.TryParse(input, out var parsed)) + { + value = parsed; + return true; + } + + value = default; + return false; + }, + + [typeof(FileInfo)] = (string path, out object? value) => + { + value = new FileInfo(path); + return true; + }, + + [typeof(FileSystemInfo)] = (string path, out object? value) => + { + if (Directory.Exists(path)) + { + value = new DirectoryInfo(path); + } + else if (path.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal) || + path.EndsWith(Path.AltDirectorySeparatorChar.ToString(), StringComparison.Ordinal)) + { + value = new DirectoryInfo(path); + } + else + { + value = new FileInfo(path); + } + + return true; + }, + + [typeof(float)] = (string input, out object? value) => + { + if (float.TryParse(input, out var parsed)) + { + value = parsed; + return true; + } + + value = default; + return false; + }, + + [typeof(int)] = (string token, out object? value) => + { + if (int.TryParse(token, out var intValue)) + { + value = intValue; + return true; + } + + value = default; + return false; + }, + + [typeof(int?)] = (string token, out object? value) => + { + if (int.TryParse(token, out var intValue)) + { + value = intValue; + return true; + } + + value = default; + return false; + }, + + [typeof(string)] = (string input, out object? value) => + { + value = input; + return true; + }, + + [typeof(Uri)] = (string input, out object? value) => + { + if (Uri.TryCreate(input, UriKind.RelativeOrAbsolute, out var uri)) + { + value = uri; + return true; + } + + value = default; + return false; + }, + }; +} \ No newline at end of file diff --git a/src/System.CommandLine/Binding/ArgumentConverter.cs b/src/System.CommandLine/Binding/ArgumentConverter.cs index e0df83531e..43ebac4334 100644 --- a/src/System.CommandLine/Binding/ArgumentConverter.cs +++ b/src/System.CommandLine/Binding/ArgumentConverter.cs @@ -4,109 +4,14 @@ using System.Collections; using System.Collections.Generic; using System.CommandLine.Parsing; -using System.ComponentModel; using System.Diagnostics.CodeAnalysis; -using System.IO; using System.Linq; -using System.Reflection; -using System.Threading; using static System.CommandLine.Binding.ArgumentConversionResult; namespace System.CommandLine.Binding { - internal static class ArgumentConverter + internal static partial class ArgumentConverter { - private static Lazy EnumerableEmptyMethod { get; } = new - (() => typeof(Enumerable).GetMethod(nameof(Array.Empty)), LazyThreadSafetyMode.None); - - private static readonly Dictionary _stringConverters = new() - { - [typeof(DirectoryInfo)] = (string path, out object? value) => - { - value = new DirectoryInfo(path); - return true; - }, - - [typeof(int)] = (string token, out object? value) => - { - if (int.TryParse(token, out var intValue)) - { - value = intValue; - return true; - } - - value = default; - return false; - }, - - [typeof(int?)] = (string token, out object? value) => - { - if (int.TryParse(token, out var intValue)) - { - value = intValue; - return true; - } - - value = default; - return false; - }, - - [typeof(bool)] = (string token, out object? value) => - { - if (bool.TryParse(token, out var parsed)) - { - value = parsed; - return true; - } - - value = default; - return false; - }, - - [typeof(bool?)] = (string token, out object? value) => - { - if (bool.TryParse(token, out var parsed)) - { - value = parsed; - return true; - } - - value = default; - return false; - }, - - [typeof(FileSystemInfo)] = (string path, out object? value) => - { - if (Directory.Exists(path)) - { - value = new DirectoryInfo(path); - } - else if (path.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal) || - path.EndsWith(Path.AltDirectorySeparatorChar.ToString(), StringComparison.Ordinal)) - { - value = new DirectoryInfo(path); - } - else - { - value = new FileInfo(path); - } - - return true; - }, - - [typeof(FileInfo)] = (string path, out object? value) => - { - value = new FileInfo(path); - return true; - }, - - [typeof(string)] = (string input, out object? value) => - { - value = input; - return true; - }, - }; - private delegate bool TryConvertString(string token, out object? value); internal static ArgumentConversionResult ConvertObject( @@ -118,7 +23,7 @@ internal static ArgumentConversionResult ConvertObject( switch (value) { case string singleValue: - if (type.IsEnumerable() && !type.HasStringTypeConverter()) + if (type.IsEnumerable()) { return ConvertStrings(argument, type, new[] { singleValue }, localizationResources); } @@ -152,32 +57,16 @@ private static ArgumentConversionResult ConvertString( } } - if (TypeDescriptor.GetConverter(type) is { } typeConverter) + if (type.IsEnum) { - if (typeConverter.CanConvertFrom(typeof(string))) + try { - try - { - return Success( - argument, - typeConverter.ConvertFromInvariantString(value)); - } - catch (Exception) - { - return Failure(argument, type, value, localizationResources); - } + return Success(argument, Enum.Parse(type, value, true)); } - } - - if (type.TryFindConstructorWithSingleParameterOfType( - typeof(string), out ConstructorInfo? ctor)) - { - var instance = ctor.Invoke(new object[] + catch (ArgumentException) { - value - }); - - return Success(argument, instance); + // TODO: (ConvertString) find a way to do this without the try..catch + } } return Failure(argument, type, value, localizationResources); @@ -194,20 +83,16 @@ public static ArgumentConversionResult ConvertStrings( if (type == typeof(string)) { + type = typeof(string[]); itemType = typeof(string); } - else if (type == typeof(bool)) - { - itemType = typeof(bool); - } else { itemType = type.GetElementTypeIfEnumerable() ?? typeof(string); } - var (values, isArray) = type.IsArray - ? (CreateArray(itemType, tokens.Count), true) - : (CreateList(itemType, tokens.Count), false); + var values = CreateEnumerable(type, itemType, tokens.Count); + var isArray = values is Array; for (var i = 0; i < tokens.Count; i++) { @@ -223,7 +108,6 @@ public static ArgumentConversionResult ConvertStrings( { argumentResult.OnlyTake(i); - // exit the for loop i = tokens.Count; break; } @@ -245,32 +129,6 @@ public static ArgumentConversionResult ConvertStrings( } return Success(argument, values); - - static IList CreateList(Type itemType, int capacity) - { - if (itemType == typeof(string)) - { - return new List(capacity); - } - else - { - return (IList)Activator.CreateInstance( - typeof(List<>).MakeGenericType(itemType), - capacity); - } - } - - static IList CreateArray(Type itemType, int capacity) - { - if (itemType == typeof(string)) - { - return new string[capacity]; - } - else - { - return Array.CreateInstance(itemType, capacity); - } - } } internal static TryConvertArgument? GetConverter(Argument argument) @@ -326,33 +184,6 @@ private static bool CanBeBoundFromScalarValue(this Type type) } } - private static bool TryFindConstructorWithSingleParameterOfType( - this Type type, - Type parameterType, - [NotNullWhen(true)] out ConstructorInfo? ctor) - { - var (x, _) = type.GetConstructors() - .Select(c => (ctor: c, parameters: c.GetParameters())) - .SingleOrDefault(tuple => tuple.ctor.IsPublic && - tuple.parameters.Length == 1 && - tuple.parameters[0].ParameterType == parameterType); - - if (x is not null) - { - ctor = x; - return true; - } - else - { - ctor = null; - return false; - } - } - - private static bool HasStringTypeConverter(this Type type) => - TypeDescriptor.GetConverter(type) is { } typeConverter && - typeConverter.CanConvertFrom(typeof(string)); - private static FailedArgumentConversionResult Failure( Argument argument, Type expectedType, @@ -381,15 +212,13 @@ internal static ArgumentConversionResult ConvertIfNeeded( typeof(IEnumerable), successful.Value, symbolResult.LocalizationResources), - NoArgumentConversionResult _ when toType == typeof(bool) => - Success(conversionResult.Argument, - true), + NoArgumentConversionResult _ when toType == typeof(bool) || toType == typeof(bool?) => + Success(conversionResult.Argument, true), NoArgumentConversionResult _ when conversionResult.Argument.Arity.MinimumNumberOfValues > 0 => new MissingArgumentConversionResult(conversionResult.Argument, symbolResult.LocalizationResources.RequiredArgumentMissing(symbolResult)), NoArgumentConversionResult _ when conversionResult.Argument.Arity.MaximumNumberOfValues > 1 => - Success(conversionResult.Argument, - Array.Empty()), + Success(conversionResult.Argument, Array.Empty()), _ => conversionResult }; } @@ -446,50 +275,26 @@ public static bool TryConvertArgument(ArgumentResult argumentResult, out object? internal static object? GetDefaultValue(Type type) { - if (type.GetElementTypeIfEnumerable() is { } itemType) + if (type.IsNullable()) { - if (type.IsArray) - { - return CreateEmptyArray(itemType); - } + return null; + } - if (type.IsGenericType) - { - return type.GetGenericTypeDefinition() switch - { - { } enumerable when enumerable == typeof(IEnumerable<>) => CreateEmptyEnumerable(itemType), - { } list when list == typeof(List<>) => CreateEmptyList(itemType), - { } array when array == typeof(IList<>) || - array == typeof(ICollection<>) => CreateEmptyArray(itemType), - _ => null - }; - } + if (type.GetElementTypeIfEnumerable() is { } itemType) + { + return CreateEnumerable(type, itemType); } return type switch { - { } nonGeneric + { } nonGeneric when nonGeneric == typeof(IList) || nonGeneric == typeof(ICollection) || nonGeneric == typeof(IEnumerable) => CreateEmptyArray(typeof(object)), - _ when type.IsValueType => Activator.CreateInstance(type), + _ when type.IsValueType => CreateDefaultValueType(type), _ => null }; - - static object CreateEmptyList(Type itemType) - { - return Activator.CreateInstance(typeof(List<>).MakeGenericType(itemType)); - } - - static IEnumerable CreateEmptyEnumerable(Type itemType) - { - var genericMethod = EnumerableEmptyMethod.Value.MakeGenericMethod(itemType); - return (IEnumerable)genericMethod.Invoke(null, new object[0]); - } - - static Array CreateEmptyArray(Type itemType) - => Array.CreateInstance(itemType, 0); } } } \ No newline at end of file diff --git a/src/System.CommandLine/Binding/IValueDescriptor.cs b/src/System.CommandLine/Binding/IValueDescriptor.cs index 34b9e0052d..94d8964c90 100644 --- a/src/System.CommandLine/Binding/IValueDescriptor.cs +++ b/src/System.CommandLine/Binding/IValueDescriptor.cs @@ -1,6 +1,8 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Diagnostics.CodeAnalysis; + namespace System.CommandLine.Binding { /// diff --git a/src/System.CommandLine/Binding/TypeExtensions.cs b/src/System.CommandLine/Binding/TypeExtensions.cs index 14627b4294..1e9be73bbf 100644 --- a/src/System.CommandLine/Binding/TypeExtensions.cs +++ b/src/System.CommandLine/Binding/TypeExtensions.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections; -using System.Linq; namespace System.CommandLine.Binding { @@ -15,25 +14,19 @@ internal static class TypeExtensions return type.GetElementType(); } - Type enumerableInterface; - - if (type.IsEnumerable()) - { - enumerableInterface = type; - } - else + if (type == typeof(string)) { - enumerableInterface = type - .GetInterfaces() - .FirstOrDefault(IsEnumerable); + return null; } - if (enumerableInterface is null) + Type? enumerableInterface = null; + + if (type.IsEnumerable()) { - return null; + enumerableInterface = type; } - return enumerableInterface.GenericTypeArguments switch + return enumerableInterface?.GenericTypeArguments switch { { Length: 1 } genericTypeArguments => genericTypeArguments[0], _ => null diff --git a/src/System.CommandLine/Builder/CommandLineBuilderExtensions.cs b/src/System.CommandLine/Builder/CommandLineBuilderExtensions.cs index 160004a0c6..7ba93b5c72 100644 --- a/src/System.CommandLine/Builder/CommandLineBuilderExtensions.cs +++ b/src/System.CommandLine/Builder/CommandLineBuilderExtensions.cs @@ -25,15 +25,18 @@ public static class CommandLineBuilderExtensions new(() => { var assembly = RootCommand.GetAssembly(); + var assemblyVersionAttribute = assembly.GetCustomAttribute(); + if (assemblyVersionAttribute is null) { - return assembly.GetName().Version.ToString(); + return assembly.GetName().Version?.ToString() ?? ""; } else { return assemblyVersionAttribute.InformationalVersion; } + }); /// @@ -183,7 +186,7 @@ await feature.EnsureRegistered(async () => try { - var currentProcessFullPath = Diagnostics.Process.GetCurrentProcess().MainModule.FileName; + var currentProcessFullPath = Diagnostics.Process.GetCurrentProcess().MainModule?.FileName; var currentProcessFileNameWithoutExtension = Path.GetFileNameWithoutExtension(currentProcessFullPath); var dotnetSuggestProcess = Process.StartProcess( diff --git a/src/System.CommandLine/DictionaryExtensions.cs b/src/System.CommandLine/DictionaryExtensions.cs index 6532c48a1c..ba4e4d7ac3 100644 --- a/src/System.CommandLine/DictionaryExtensions.cs +++ b/src/System.CommandLine/DictionaryExtensions.cs @@ -9,7 +9,7 @@ public static TValue GetOrAdd( TKey key, Func create) { - if (source.TryGetValue(key, out TValue value)) + if (source.TryGetValue(key, out TValue? value)) { return value; } diff --git a/src/System.CommandLine/EnumerableExtensions.cs b/src/System.CommandLine/EnumerableExtensions.cs index 4ddb8f7942..2aecce2477 100644 --- a/src/System.CommandLine/EnumerableExtensions.cs +++ b/src/System.CommandLine/EnumerableExtensions.cs @@ -36,15 +36,15 @@ internal static IEnumerable FlattenBreadthFirst( } internal static IEnumerable RecurseWhileNotNull( - this T source, - Func next) + this T? source, + Func next) where T : class { - yield return source; - - while ((source = next(source!)) is not null) + while (source is not null) { yield return source; + + source = next(source); } } } diff --git a/src/System.CommandLine/Help/HelpBuilder.Default.cs b/src/System.CommandLine/Help/HelpBuilder.Default.cs index cd4488712a..89c6796d66 100644 --- a/src/System.CommandLine/Help/HelpBuilder.Default.cs +++ b/src/System.CommandLine/Help/HelpBuilder.Default.cs @@ -32,7 +32,7 @@ public static string GetArgumentDefaultValue(Argument argument) } else { - return defaultValue.ToString(); + return defaultValue.ToString() ?? ""; } } } diff --git a/src/System.CommandLine/Help/HelpBuilder.cs b/src/System.CommandLine/Help/HelpBuilder.cs index b4359894c6..f10efc7e3b 100644 --- a/src/System.CommandLine/Help/HelpBuilder.cs +++ b/src/System.CommandLine/Help/HelpBuilder.cs @@ -495,7 +495,7 @@ private string GetArgumentDefaultValue( if (_customizationsBySymbol is not null) { - if (_customizationsBySymbol.TryGetValue(parent, out Customization customization) && + if (_customizationsBySymbol.TryGetValue(parent, out var customization) && customization.GetDefaultValue?.Invoke(context) is { } parentDefaultValue) { displayedDefaultValue = parentDefaultValue; diff --git a/src/System.CommandLine/Help/HelpOption.cs b/src/System.CommandLine/Help/HelpOption.cs index 8320b085a5..9033dc11af 100644 --- a/src/System.CommandLine/Help/HelpOption.cs +++ b/src/System.CommandLine/Help/HelpOption.cs @@ -36,7 +36,7 @@ public override string? Description internal override bool IsGreedy => false; - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is HelpOption; } diff --git a/src/System.CommandLine/Help/VersionOption.cs b/src/System.CommandLine/Help/VersionOption.cs index 3898f7467d..31ea392161 100644 --- a/src/System.CommandLine/Help/VersionOption.cs +++ b/src/System.CommandLine/Help/VersionOption.cs @@ -63,7 +63,7 @@ public override string? Description internal override bool IsGreedy => false; - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is VersionOption; } diff --git a/src/System.CommandLine/IO/IStandardStreamWriter.cs b/src/System.CommandLine/IO/IStandardStreamWriter.cs index 657670d26a..e21efa93a5 100644 --- a/src/System.CommandLine/IO/IStandardStreamWriter.cs +++ b/src/System.CommandLine/IO/IStandardStreamWriter.cs @@ -12,6 +12,6 @@ public interface IStandardStreamWriter /// Writes the specified string to the stream. /// /// The value to write. - void Write(string value); + void Write(string? value); } } \ No newline at end of file diff --git a/src/System.CommandLine/IO/StandardStreamWriter.cs b/src/System.CommandLine/IO/StandardStreamWriter.cs index 1c4d66220e..64f0c37843 100644 --- a/src/System.CommandLine/IO/StandardStreamWriter.cs +++ b/src/System.CommandLine/IO/StandardStreamWriter.cs @@ -83,7 +83,7 @@ public override void Write(char value) _writer.Write(value.ToString()); } - public override void Write(string value) + public override void Write(string? value) { _writer.Write(value); } @@ -91,14 +91,14 @@ public override void Write(string value) private class AnonymousStandardStreamWriter : IStandardStreamWriter { - private readonly Action _write; + private readonly Action _write; - public AnonymousStandardStreamWriter(Action write) + public AnonymousStandardStreamWriter(Action write) { _write = write; } - public void Write(string value) + public void Write(string? value) { _write(value); } diff --git a/src/System.CommandLine/IO/SystemConsole.cs b/src/System.CommandLine/IO/SystemConsole.cs index 622ce3be00..15cb6e59c3 100644 --- a/src/System.CommandLine/IO/SystemConsole.cs +++ b/src/System.CommandLine/IO/SystemConsole.cs @@ -38,14 +38,14 @@ private struct StandardErrorStreamWriter : IStandardStreamWriter { public static readonly StandardErrorStreamWriter Instance = new(); - public void Write(string value) => Console.Error.Write(value); + public void Write(string? value) => Console.Error.Write(value); } private struct StandardOutStreamWriter : IStandardStreamWriter { public static readonly StandardOutStreamWriter Instance = new(); - public void Write(string value) => Console.Out.Write(value); + public void Write(string? value) => Console.Out.Write(value); } } } \ No newline at end of file diff --git a/src/System.CommandLine/Invocation/FeatureRegistration.cs b/src/System.CommandLine/Invocation/FeatureRegistration.cs index 7e4844a352..7f6cda7ab1 100644 --- a/src/System.CommandLine/Invocation/FeatureRegistration.cs +++ b/src/System.CommandLine/Invocation/FeatureRegistration.cs @@ -8,7 +8,7 @@ namespace System.CommandLine.Invocation { internal class FeatureRegistration { - private static readonly string _assemblyName = RootCommand.GetAssembly().FullName; + private static readonly string? _assemblyName = RootCommand.GetAssembly().FullName; private readonly FileInfo _sentinelFile; @@ -25,7 +25,7 @@ public async Task EnsureRegistered(Func> onInitialize) { if (!_sentinelFile.Exists) { - if (!_sentinelFile.Directory.Exists) + if (_sentinelFile.Directory is { Exists: false }) { _sentinelFile.Directory.Create(); } diff --git a/src/System.CommandLine/Parsing/ParseResult.cs b/src/System.CommandLine/Parsing/ParseResult.cs index e77c45a749..8ff44ca61a 100644 --- a/src/System.CommandLine/Parsing/ParseResult.cs +++ b/src/System.CommandLine/Parsing/ParseResult.cs @@ -328,7 +328,6 @@ public T ValueForOption(string alias) _ => throw new ArgumentOutOfRangeException(nameof(symbol)) }; - /// /// Gets completions based on a given parse result. /// @@ -369,8 +368,6 @@ static IEnumerable OptionsWithArgumentLimitReached(SymbolResult symbolRe .SelectMany(c => c.Aliases); } - - private SymbolResult SymbolToComplete(int? position = null) { var commandResult = CommandResult; diff --git a/src/System.CommandLine/Parsing/Token.cs b/src/System.CommandLine/Parsing/Token.cs index 6bbaea9c1a..ec8362c439 100644 --- a/src/System.CommandLine/Parsing/Token.cs +++ b/src/System.CommandLine/Parsing/Token.cs @@ -51,7 +51,7 @@ internal Token(string? value, TokenType type, Symbol? symbol, int position) internal Symbol? Symbol { get; } /// - public override bool Equals(object obj) => obj is Token other && Equals(other); + public override bool Equals(object? obj) => obj is Token other && Equals(other); /// public bool Equals(Token other) => Value == other.Value && Type == other.Type && ReferenceEquals(Symbol, other.Symbol); diff --git a/src/System.CommandLine/RootCommand.cs b/src/System.CommandLine/RootCommand.cs index 013520fec0..3b6d925638 100644 --- a/src/System.CommandLine/RootCommand.cs +++ b/src/System.CommandLine/RootCommand.cs @@ -49,7 +49,7 @@ public RootCommand(string description = "") : base(ExecutableName, description) { } - internal static Assembly GetAssembly() => _assembly.Value; + internal static Assembly GetAssembly() => _assembly.Value!; /// /// The name of the currently running executable. diff --git a/src/System.CommandLine/System.CommandLine.csproj b/src/System.CommandLine/System.CommandLine.csproj index 40a4b03bf5..462eeae660 100644 --- a/src/System.CommandLine/System.CommandLine.csproj +++ b/src/System.CommandLine/System.CommandLine.csproj @@ -3,7 +3,7 @@ true System.CommandLine - netstandard2.0 + net6.0;netstandard2.0 enable true 10 @@ -15,6 +15,8 @@ * Test and debug support true + true + true @@ -22,7 +24,7 @@ - + diff --git a/src/System.CommandLine/System.Diagnostics.CodeAnalysis/DynamicallyAccessedMemberTypes.cs b/src/System.CommandLine/System.Diagnostics.CodeAnalysis/DynamicallyAccessedMemberTypes.cs new file mode 100644 index 0000000000..a139bdc050 --- /dev/null +++ b/src/System.CommandLine/System.Diagnostics.CodeAnalysis/DynamicallyAccessedMemberTypes.cs @@ -0,0 +1,94 @@ +// adapted from: https://github.com/dotnet/aspnetcore/blob/404d81767784552b0a148cb8c437332ebe726ae9/src/Shared/CodeAnalysis/DynamicallyAccessedMemberTypes.cs + +#if !NET6_0_OR_GREATER +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Diagnostics.CodeAnalysis +{ + /// + /// Specifies the types of members that are dynamically accessed. + /// + /// This enumeration has a attribute that allows a + /// bitwise combination of its member values. + /// + [Flags] + internal enum DynamicallyAccessedMemberTypes + { + /// + /// Specifies no members. + /// + None = 0, + + /// + /// Specifies the default, parameterless public constructor. + /// + PublicParameterlessConstructor = 0x0001, + + /// + /// Specifies all public constructors. + /// + PublicConstructors = 0x0002 | PublicParameterlessConstructor, + + /// + /// Specifies all non-public constructors. + /// + NonPublicConstructors = 0x0004, + + /// + /// Specifies all public methods. + /// + PublicMethods = 0x0008, + + /// + /// Specifies all non-public methods. + /// + NonPublicMethods = 0x0010, + + /// + /// Specifies all public fields. + /// + PublicFields = 0x0020, + + /// + /// Specifies all non-public fields. + /// + NonPublicFields = 0x0040, + + /// + /// Specifies all public nested types. + /// + PublicNestedTypes = 0x0080, + + /// + /// Specifies all non-public nested types. + /// + NonPublicNestedTypes = 0x0100, + + /// + /// Specifies all public properties. + /// + PublicProperties = 0x0200, + + /// + /// Specifies all non-public properties. + /// + NonPublicProperties = 0x0400, + + /// + /// Specifies all public events. + /// + PublicEvents = 0x0800, + + /// + /// Specifies all non-public events. + /// + NonPublicEvents = 0x1000, + + /// + /// Specifies all members. + /// + All = ~None + } +} +#endif \ No newline at end of file diff --git a/src/System.CommandLine/System.Diagnostics.CodeAnalysis/DynamicallyAccessedMembersAttribute.cs b/src/System.CommandLine/System.Diagnostics.CodeAnalysis/DynamicallyAccessedMembersAttribute.cs new file mode 100644 index 0000000000..a39283b0ab --- /dev/null +++ b/src/System.CommandLine/System.Diagnostics.CodeAnalysis/DynamicallyAccessedMembersAttribute.cs @@ -0,0 +1,51 @@ +// adapted from: https://github.com/dotnet/aspnetcore/blob/404d81767784552b0a148cb8c437332ebe726ae9/src/Shared/CodeAnalysis/DynamicallyAccessedMembersAttribute.cs#L29 + +#if !NET6_0_OR_GREATER +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Diagnostics.CodeAnalysis +{ + /// + /// Indicates that certain members on a specified are accessed dynamically, + /// for example through . + /// + /// + /// This allows tools to understand which members are being accessed during the execution + /// of a program. + /// + /// This attribute is valid on members whose type is or . + /// + /// When this attribute is applied to a location of type , the assumption is + /// that the string represents a fully qualified type name. + /// + /// If the attribute is applied to a method it's treated as a special case and it implies + /// the attribute should be applied to the "this" parameter of the method. As such the attribute + /// should only be used on instance methods of types assignable to System.Type (or string, but no methods + /// will use it there). + /// + [AttributeUsage( + AttributeTargets.Field | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter | + AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.Method, + Inherited = false)] + internal sealed class DynamicallyAccessedMembersAttribute : Attribute + { + /// + /// Initializes a new instance of the class + /// with the specified member types. + /// + /// The types of members dynamically accessed. + public DynamicallyAccessedMembersAttribute(DynamicallyAccessedMemberTypes memberTypes) + { + MemberTypes = memberTypes; + } + + /// + /// Gets the which specifies the type + /// of members dynamically accessed. + /// + public DynamicallyAccessedMemberTypes MemberTypes { get; } + } +} + +#endif \ No newline at end of file diff --git a/src/System.CommandLine/System.Diagnostics.CodeAnalysis/UnconditionalSuppressMessageAttribute.cs b/src/System.CommandLine/System.Diagnostics.CodeAnalysis/UnconditionalSuppressMessageAttribute.cs new file mode 100644 index 0000000000..8c54ed2c91 --- /dev/null +++ b/src/System.CommandLine/System.Diagnostics.CodeAnalysis/UnconditionalSuppressMessageAttribute.cs @@ -0,0 +1,97 @@ +// adapted from: https://github.com/dotnet/runtime/blob/a5159b1a8840632ad34cf59c5aaf77040cb6ceda/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/UnconditionalSuppressMessageAttribute.cs#L21 + +#if !NET6_0_OR_GREATER + + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Diagnostics.CodeAnalysis +{ + /// + /// Suppresses reporting of a specific rule violation, allowing multiple suppressions on a + /// single code artifact. + /// + /// + /// is different than + /// in that it doesn't have a + /// . So it is always preserved in the compiled assembly. + /// + [AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)] +#if SYSTEM_PRIVATE_CORELIB + public +#else + internal +#endif + sealed class UnconditionalSuppressMessageAttribute : Attribute + { + /// + /// Initializes a new instance of the + /// class, specifying the category of the tool and the identifier for an analysis rule. + /// + /// The category for the attribute. + /// The identifier of the analysis rule the attribute applies to. + public UnconditionalSuppressMessageAttribute(string category, string checkId) + { + Category = category; + CheckId = checkId; + } + + /// + /// Gets the category identifying the classification of the attribute. + /// + /// + /// The property describes the tool or tool analysis category + /// for which a message suppression attribute applies. + /// + public string Category { get; } + + /// + /// Gets the identifier of the analysis tool rule to be suppressed. + /// + /// + /// Concatenated together, the and + /// properties form a unique check identifier. + /// + public string CheckId { get; } + + /// + /// Gets or sets the scope of the code that is relevant for the attribute. + /// + /// + /// The Scope property is an optional argument that specifies the metadata scope for which + /// the attribute is relevant. + /// + public string? Scope { get; set; } + + /// + /// Gets or sets a fully qualified path that represents the target of the attribute. + /// + /// + /// The property is an optional argument identifying the analysis target + /// of the attribute. An example value is "System.IO.Stream.ctor():System.Void". + /// Because it is fully qualified, it can be long, particularly for targets such as parameters. + /// The analysis tool user interface should be capable of automatically formatting the parameter. + /// + public string? Target { get; set; } + + /// + /// Gets or sets an optional argument expanding on exclusion criteria. + /// + /// + /// The property is an optional argument that specifies additional + /// exclusion where the literal metadata target is not sufficiently precise. For example, + /// the cannot be applied within a method, + /// and it may be desirable to suppress a violation against a statement in the method that will + /// give a rule violation, but not against all statements in the method. + /// + public string? MessageId { get; set; } + + /// + /// Gets or sets the justification for suppressing the code analysis message. + /// + public string? Justification { get; set; } + } +} + +#endif diff --git a/src/System.CommandLine/System.Runtime.CompilerServices/IsExternalInit.cs b/src/System.CommandLine/System.Runtime.CompilerServices/IsExternalInit.cs index fbdfe63e1c..aee0d3ca7e 100644 --- a/src/System.CommandLine/System.Runtime.CompilerServices/IsExternalInit.cs +++ b/src/System.CommandLine/System.Runtime.CompilerServices/IsExternalInit.cs @@ -1,20 +1,8 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace System.Runtime.CompilerServices -{ - internal static class IsExternalInit - { - } - - [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] - internal sealed class CallerArgumentExpressionAttribute : Attribute - { - public CallerArgumentExpressionAttribute(string parameterName) - { - ParameterName = parameterName; - } +namespace System.Runtime.CompilerServices; - public string ParameterName { get; } - } +internal static class IsExternalInit +{ } \ No newline at end of file diff --git a/src/System.Diagnostics.CodeAnalysis.cs b/src/System.Diagnostics.CodeAnalysis.cs index 8df0ee9dd7..8a5b242094 100644 --- a/src/System.Diagnostics.CodeAnalysis.cs +++ b/src/System.Diagnostics.CodeAnalysis.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +#if !NET6_0_OR_GREATER + #pragma warning disable CA1801, CA1822 namespace System.Diagnostics.CodeAnalysis @@ -67,4 +69,6 @@ public NotNullWhenAttribute(bool returnValue) { } public bool ReturnValue { get { throw null!; } } } -} \ No newline at end of file +} + +#endif \ No newline at end of file