diff --git a/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_api_is_not_changed.approved.txt b/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_api_is_not_changed.approved.txt index 7ad424075e..8e65778e1c 100644 --- a/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_api_is_not_changed.approved.txt +++ b/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_api_is_not_changed.approved.txt @@ -78,8 +78,6 @@ System.CommandLine public System.Void ThrowIfInvalid() public class CommandLineConfigurationException : System.Exception, System.Runtime.Serialization.ISerializable .ctor(System.String message) - .ctor() - .ctor(System.String message, System.Exception innerException) public static class CompletionSourceExtensions public static System.Void Add(this System.Collections.Generic.List>> completionSources, System.Func> completionsDelegate) public static System.Void Add(this System.Collections.Generic.List>> completionSources, System.String[] completions) diff --git a/src/System.CommandLine.NamingConventionBinder/ParameterDescriptor.cs b/src/System.CommandLine.NamingConventionBinder/ParameterDescriptor.cs index 4cc264117b..230a82e3e3 100644 --- a/src/System.CommandLine.NamingConventionBinder/ParameterDescriptor.cs +++ b/src/System.CommandLine.NamingConventionBinder/ParameterDescriptor.cs @@ -58,7 +58,7 @@ internal static bool CalculateAllowsNull(ParameterInfo parameterInfo) /// public object? GetDefaultValue() => _parameterInfo.DefaultValue is DBNull - ? ArgumentConverter.GetDefaultValue(ValueType) + ? ValueType.GetDefaultValue() : _parameterInfo.DefaultValue; /// diff --git a/src/System.CommandLine.NamingConventionBinder/PropertyDescriptor.cs b/src/System.CommandLine.NamingConventionBinder/PropertyDescriptor.cs index 7600731fc8..cd089590b1 100644 --- a/src/System.CommandLine.NamingConventionBinder/PropertyDescriptor.cs +++ b/src/System.CommandLine.NamingConventionBinder/PropertyDescriptor.cs @@ -36,7 +36,7 @@ internal PropertyDescriptor( public bool HasDefaultValue => false; /// - public object? GetDefaultValue() => ArgumentConverter.GetDefaultValue(ValueType); + public object? GetDefaultValue() => ValueType.GetDefaultValue(); /// /// Sets a value on the target property. diff --git a/src/System.CommandLine.NamingConventionBinder/TypeExtensions.cs b/src/System.CommandLine.NamingConventionBinder/TypeExtensions.cs index 7ce8518909..5875df3049 100644 --- a/src/System.CommandLine.NamingConventionBinder/TypeExtensions.cs +++ b/src/System.CommandLine.NamingConventionBinder/TypeExtensions.cs @@ -1,7 +1,10 @@ // 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.CommandLine.Binding; using System.Runtime.CompilerServices; +using System.Runtime.Serialization; namespace System.CommandLine.NamingConventionBinder; @@ -28,4 +31,28 @@ public static bool IsConstructedGenericTypeOf(this Type type, Type genericTypeDe [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsNullableValueType(this Type type) => type.IsValueType && type.IsConstructedGenericTypeOf(typeof(Nullable<>)); + + internal static object? GetDefaultValue(this Type type) + { + if (type.IsNullable()) + { + return null; + } + + if (type.GetElementTypeIfEnumerable() is { } itemType) + { + return ArgumentConverter.CreateEnumerable(type, itemType); + } + + return type switch + { + { } nonGeneric + when nonGeneric == typeof(IList) || + nonGeneric == typeof(ICollection) || + nonGeneric == typeof(IEnumerable) + => Array.Empty(), + _ when type.IsValueType => FormatterServices.GetUninitializedObject(type), + _ => null + }; + } } \ No newline at end of file diff --git a/src/System.CommandLine.Tests/Binding/TypeConversionTests.cs b/src/System.CommandLine.Tests/Binding/TypeConversionTests.cs index 7a1f991eb5..bbb0337dba 100644 --- a/src/System.CommandLine.Tests/Binding/TypeConversionTests.cs +++ b/src/System.CommandLine.Tests/Binding/TypeConversionTests.cs @@ -531,8 +531,15 @@ public void Values_can_be_correctly_converted_to_nullable_TimeSpan_without_the_p } [Fact] - public void Values_can_be_correctly_converted_to_Uri_without_the_parser_specifying_a_custom_converter() - => GetValue(new Option("-x"), "-x http://example.com").Should().BeEquivalentTo(new Uri("http://example.com")); + public void Values_can_be_correctly_converted_to_Uri_when_custom_parser_is_provided() + { + Option option = new ("-x") + { + CustomParser = (argumentResult) => Uri.TryCreate(argumentResult.Tokens.Last().Value, UriKind.RelativeOrAbsolute, out var uri) ? uri : null + }; + + GetValue(option, "-x http://example.com").Should().BeEquivalentTo(new Uri("http://example.com")); + } [Fact] public void Options_with_arguments_specified_can_be_correctly_converted_to_bool_without_the_parser_specifying_a_custom_converter() @@ -582,13 +589,27 @@ public void Values_can_be_correctly_converted_to_nullable_sbyte_without_the_pars => GetValue(new Option("-us"), "-us 123").Should().Be(123); [Fact] - public void Values_can_be_correctly_converted_to_ipaddress_without_the_parser_specifying_a_custom_converter() - => GetValue(new Option("-us"), "-us 1.2.3.4").Should().Be(IPAddress.Parse("1.2.3.4")); + public void Values_can_be_correctly_converted_to_ipaddress_when_custom_parser_is_provided() + { + Option option = new ("-us") + { + CustomParser = (argumentResult) => IPAddress.Parse(argumentResult.Tokens.Last().Value) + }; + + GetValue(option, "-us 1.2.3.4").Should().Be(IPAddress.Parse("1.2.3.4")); + } #if NETCOREAPP3_0_OR_GREATER [Fact] - public void Values_can_be_correctly_converted_to_ipendpoint_without_the_parser_specifying_a_custom_converter() - => GetValue(new Option("-us"), "-us 1.2.3.4:56").Should().Be(IPEndPoint.Parse("1.2.3.4:56")); + public void Values_can_be_correctly_converted_to_ipendpoint_when_custom_parser_is_provided() + { + Option option = new("-us") + { + CustomParser = (argumentResult) => IPEndPoint.Parse(argumentResult.Tokens.Last().Value) + }; + + GetValue(option, "-us 1.2.3.4:56").Should().Be(IPEndPoint.Parse("1.2.3.4:56")); + } #endif #if NET6_0_OR_GREATER @@ -756,9 +777,6 @@ public void String_defaults_to_null_when_not_specified_only_for_not_required_arg [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), new object[] { "argName" }); diff --git a/src/System.CommandLine/Argument.cs b/src/System.CommandLine/Argument.cs index 5cd46ef9b4..2462cebc4e 100644 --- a/src/System.CommandLine/Argument.cs +++ b/src/System.CommandLine/Argument.cs @@ -54,11 +54,40 @@ internal TryConvertArgument? ConvertArguments /// /// Gets the list of completion sources for the argument. /// - public List>> CompletionSources => - _completionSources ??= new () + public List>> CompletionSources + { + get { - CompletionSource.ForType(ValueType) - }; + if (_completionSources is null) + { + Type? valueType = ValueType; + if (valueType == typeof(bool) || valueType == typeof(bool?)) + { + _completionSources = new () + { + static _ => new CompletionItem[] + { + new(bool.TrueString), + new(bool.FalseString) + } + }; + } + else if (!valueType.IsPrimitive && (valueType.IsEnum || (valueType.TryGetNullableType(out valueType) && valueType.IsEnum))) + { + _completionSources = new() + { + _ => Enum.GetNames(valueType).Select(n => new CompletionItem(n)) + }; + } + else + { + _completionSources = new(); + } + } + + return _completionSources; + } + } /// /// Gets or sets the that the argument token(s) will be converted to. diff --git a/src/System.CommandLine/Argument{T}.cs b/src/System.CommandLine/Argument{T}.cs index 324e5e5091..acc27e8aec 100644 --- a/src/System.CommandLine/Argument{T}.cs +++ b/src/System.CommandLine/Argument{T}.cs @@ -1,7 +1,9 @@ // 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.CommandLine.Parsing; +using System.Diagnostics.CodeAnalysis; using System.IO; namespace System.CommandLine @@ -160,5 +162,36 @@ public void AcceptLegalFileNamesOnly() } }); } + + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050", Justification = "https://github.com/dotnet/command-line-api/issues/1638")] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2091", Justification = "https://github.com/dotnet/command-line-api/issues/1638")] + internal static T? CreateDefaultValue() + { + if (default(T) is null && typeof(T) != typeof(string)) + { + if (typeof(T).IsArray) + { + return (T?)(object)Array.CreateInstance(typeof(T).GetElementType()!, 0); + } + else if (typeof(T).IsGenericType) + { + var genericTypeDefinition = typeof(T).GetGenericTypeDefinition(); + + if (genericTypeDefinition == typeof(IEnumerable<>) || + genericTypeDefinition == typeof(IList<>) || + genericTypeDefinition == typeof(ICollection<>)) + { + return (T?)(object)Array.CreateInstance(typeof(T).GenericTypeArguments[0], 0); + } + + if (genericTypeDefinition == typeof(List<>)) + { + return Activator.CreateInstance(); + } + } + } + + return default; + } } } diff --git a/src/System.CommandLine/Binding/ArgumentConverter.DefaultValues.cs b/src/System.CommandLine/Binding/ArgumentConverter.DefaultValues.cs index b569a2ffef..a44a8114d9 100644 --- a/src/System.CommandLine/Binding/ArgumentConverter.DefaultValues.cs +++ b/src/System.CommandLine/Binding/ArgumentConverter.DefaultValues.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Reflection; -using System.Runtime.Serialization; namespace System.CommandLine.Binding; @@ -37,7 +36,7 @@ private static IList CreateEmptyList(Type listType) return (IList)ctor.Invoke(null); } - private static IList CreateEnumerable(Type type, Type itemType, int capacity = 0) + internal static IList CreateEnumerable(Type type, Type itemType, int capacity = 0) { if (type.IsArray) { @@ -63,9 +62,4 @@ private static IList CreateEnumerable(Type type, Type itemType, int capacity = 0 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 index 0846fb7a3c..4a48afbb34 100644 --- a/src/System.CommandLine/Binding/ArgumentConverter.StringConverters.cs +++ b/src/System.CommandLine/Binding/ArgumentConverter.StringConverters.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; -using System.Net; namespace System.CommandLine.Binding; @@ -11,7 +10,10 @@ internal static partial class ArgumentConverter { private delegate bool TryConvertString(string token, out object? value); - private static readonly Dictionary _stringConverters = new() + private static Dictionary? _stringConverters; + + private static Dictionary StringConverters + => _stringConverters ??= new() { [typeof(bool)] = (string token, out object? value) => { @@ -169,32 +171,6 @@ internal static partial class ArgumentConverter return false; }, - [typeof(IPAddress)] = (string token, out object? value) => - { - if (IPAddress.TryParse(token, out var ip)) - { - value = ip; - return true; - } - - value = default; - return false; - }, - -#if NETCOREAPP3_0_OR_GREATER - [typeof(IPEndPoint)] = (string token, out object? value) => - { - if (IPEndPoint.TryParse(token, out var ipendpoint)) - { - value = ipendpoint; - return true; - } - - value = default; - return false; - }, -#endif - [typeof(long)] = (string token, out object? value) => { if (long.TryParse(token, out var longValue)) @@ -299,18 +275,6 @@ internal static partial class ArgumentConverter return false; }, - [typeof(Uri)] = (string input, out object? value) => - { - if (Uri.TryCreate(input, UriKind.RelativeOrAbsolute, out var uri)) - { - value = uri; - return true; - } - - value = default; - return false; - }, - [typeof(TimeSpan)] = (string input, out object? value) => { if (TimeSpan.TryParse(input, out var timeSpan)) diff --git a/src/System.CommandLine/Binding/ArgumentConverter.cs b/src/System.CommandLine/Binding/ArgumentConverter.cs index cb2f952b94..cea514e48d 100644 --- a/src/System.CommandLine/Binding/ArgumentConverter.cs +++ b/src/System.CommandLine/Binding/ArgumentConverter.cs @@ -1,7 +1,6 @@ // 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.CommandLine.Parsing; using static System.CommandLine.Binding.ArgumentConversionResult; @@ -40,7 +39,7 @@ private static ArgumentConversionResult ConvertToken( return ConvertToken(argumentResult, nullableType, token); } - if (_stringConverters.TryGetValue(type, out var tryConvert)) + if (StringConverters.TryGetValue(type, out var tryConvert)) { if (tryConvert(value, out var converted)) { @@ -123,12 +122,12 @@ private static ArgumentConversionResult ConvertTokens( if (argument.Arity is { MaximumNumberOfValues: 1, MinimumNumberOfValues: 1 }) { if (argument.ValueType.TryGetNullableType(out var nullableType) && - _stringConverters.TryGetValue(nullableType, out var convertNullable)) + StringConverters.TryGetValue(nullableType, out var convertNullable)) { return (ArgumentResult result, out object? value) => ConvertSingleString(result, convertNullable, out value); } - if (_stringConverters.TryGetValue(argument.ValueType, out var convert1)) + if (StringConverters.TryGetValue(argument.ValueType, out var convert1)) { return (ArgumentResult result, out object? value) => ConvertSingleString(result, convert1, out value); } @@ -224,29 +223,5 @@ public static bool TryConvertArgument(ArgumentResult argumentResult, out object? value = result; return result.Result == ArgumentConversionResultType.Successful; } - - internal static object? GetDefaultValue(Type type) - { - if (type.IsNullable()) - { - return null; - } - - if (type.GetElementTypeIfEnumerable() is { } itemType) - { - return CreateEnumerable(type, itemType); - } - - return type switch - { - { } nonGeneric - when nonGeneric == typeof(IList) || - nonGeneric == typeof(ICollection) || - nonGeneric == typeof(IEnumerable) - => Array.Empty(), - _ when type.IsValueType => CreateDefaultValueType(type), - _ => null - }; - } } } \ No newline at end of file diff --git a/src/System.CommandLine/CommandLineConfigurationException.cs b/src/System.CommandLine/CommandLineConfigurationException.cs index 1263a5535d..35a49e88a6 100644 --- a/src/System.CommandLine/CommandLineConfigurationException.cs +++ b/src/System.CommandLine/CommandLineConfigurationException.cs @@ -1,33 +1,15 @@ // 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.Runtime.Serialization; - namespace System.CommandLine; /// /// Indicates that a command line configuration is invalid. /// -[Serializable] public class CommandLineConfigurationException : Exception { /// public CommandLineConfigurationException(string message) : base(message) { } - - /// - public CommandLineConfigurationException() - { - } - - /// - protected CommandLineConfigurationException(SerializationInfo info, StreamingContext context) : base(info, context) - { - } - - /// - public CommandLineConfigurationException(string message, Exception innerException) : base(message, innerException) - { - } } \ No newline at end of file diff --git a/src/System.CommandLine/Completions/CompletionSource.cs b/src/System.CommandLine/Completions/CompletionSource.cs deleted file mode 100644 index 4a716d3c28..0000000000 --- a/src/System.CommandLine/Completions/CompletionSource.cs +++ /dev/null @@ -1,46 +0,0 @@ -// 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.Concurrent; -using System.Collections.Generic; -using System.CommandLine.Binding; -using System.Linq; - -namespace System.CommandLine.Completions -{ - /// - /// Provides extension methods supporting command line tab completion. - /// - internal static class CompletionSource - { - private static readonly ConcurrentDictionary>> _completionSourcesByType = new(); - - /// - /// Gets a completion source that provides completions for a type (e.g. enum) with well-known values. - /// - internal static Func> ForType(Type type) - { - return _completionSourcesByType.GetOrAdd(type, t => GetCompletionSourceForType(t)); - } - - private static Func> GetCompletionSourceForType(Type type) - { - Type actualType = type.TryGetNullableType(out var nullableType) ? nullableType : type; - - if (actualType.IsEnum) - { - return _ => Enum.GetNames(actualType).Select(n => new CompletionItem(n)); - } - else if (actualType == typeof(bool)) - { - return static _ => new CompletionItem[] - { - new(bool.TrueString), - new(bool.FalseString) - }; - } - - return static _ => Array.Empty(); - } - } -} \ No newline at end of file diff --git a/src/System.CommandLine/Help/HelpBuilder.cs b/src/System.CommandLine/Help/HelpBuilder.cs index aebf4af73c..8f6e4794d4 100644 --- a/src/System.CommandLine/Help/HelpBuilder.cs +++ b/src/System.CommandLine/Help/HelpBuilder.cs @@ -268,7 +268,7 @@ private string FormatArgumentUsage(IList arguments) { var sb = new StringBuilder(arguments.Count * 100); - var end = default(Stack); + var end = default(List); for (var i = 0; i < arguments.Count; i++) { @@ -288,7 +288,7 @@ private string FormatArgumentUsage(IList arguments) if (isOptional) { sb.Append($"[<{argument.Name}>{arityIndicator}"); - (end ??= new Stack()).Push(']'); + (end ??= new ()).Add(']'); } else { @@ -306,7 +306,8 @@ private string FormatArgumentUsage(IList arguments) { while (end.Count > 0) { - sb.Append(end.Pop()); + sb.Append(end[end.Count - 1]); + end.RemoveAt(end.Count - 1); } } } diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index 8dff241ee8..43cd8b7d6a 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -160,7 +160,7 @@ CommandLineText is null { ArgumentResult argumentResult => Convert(argumentResult.GetArgumentConversionResult()), OptionResult optionResult => Convert(optionResult.ArgumentConversionResult), - _ => (T?)ArgumentConverter.GetDefaultValue(typeof(T)) + _ => Argument.CreateDefaultValue() }; void Populate(Dictionary cache, IList symbols) where TSymbol : Symbol diff --git a/src/System.CommandLine/Parsing/SymbolResult.cs b/src/System.CommandLine/Parsing/SymbolResult.cs index 3178d7e81c..99118c2b81 100644 --- a/src/System.CommandLine/Parsing/SymbolResult.cs +++ b/src/System.CommandLine/Parsing/SymbolResult.cs @@ -75,7 +75,7 @@ private protected SymbolResult(SymbolResultTree symbolResultTree, SymbolResult? return t; } - return (T)ArgumentConverter.GetDefaultValue(argument.ValueType)!; + return Argument.CreateDefaultValue(); } /// @@ -87,7 +87,7 @@ private protected SymbolResult(SymbolResultTree symbolResultTree, SymbolResult? return t; } - return (T)ArgumentConverter.GetDefaultValue(option.Argument.ValueType)!; + return Argument.CreateDefaultValue(); } internal virtual bool UseDefaultValueFor(ArgumentResult argumentResult) => false;