Skip to content

Commit 0760996

Browse files
authored
Remove dependencies (#2127)
* remove built-in parsers for IPAddress and IPEndPoint to reduce dependencies count and self-contained app size * remove static cache of completion sources by source type to remove dependency to System.Collections.Concurrent * remove dependency to System.Runtime.Serialization.Formatters.dll, use less reflection and drop non-generic collections support * remove built-in parser for Uri (System.Private.Uri.dll) * stop using Stack<T> because it's defined in System.Collections.dll
1 parent 7e84513 commit 0760996

15 files changed

+137
-162
lines changed

src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_api_is_not_changed.approved.txt

-2
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,6 @@ System.CommandLine
7878
public System.Void ThrowIfInvalid()
7979
public class CommandLineConfigurationException : System.Exception, System.Runtime.Serialization.ISerializable
8080
.ctor(System.String message)
81-
.ctor()
82-
.ctor(System.String message, System.Exception innerException)
8381
public static class CompletionSourceExtensions
8482
public static System.Void Add(this System.Collections.Generic.List<System.Func<System.CommandLine.Completions.CompletionContext,System.Collections.Generic.IEnumerable<System.CommandLine.Completions.CompletionItem>>> completionSources, System.Func<System.CommandLine.Completions.CompletionContext,System.Collections.Generic.IEnumerable<System.String>> completionsDelegate)
8583
public static System.Void Add(this System.Collections.Generic.List<System.Func<System.CommandLine.Completions.CompletionContext,System.Collections.Generic.IEnumerable<System.CommandLine.Completions.CompletionItem>>> completionSources, System.String[] completions)

src/System.CommandLine.NamingConventionBinder/ParameterDescriptor.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ internal static bool CalculateAllowsNull(ParameterInfo parameterInfo)
5858
/// <inheritdoc />
5959
public object? GetDefaultValue() =>
6060
_parameterInfo.DefaultValue is DBNull
61-
? ArgumentConverter.GetDefaultValue(ValueType)
61+
? ValueType.GetDefaultValue()
6262
: _parameterInfo.DefaultValue;
6363

6464
/// <inheritdoc />

src/System.CommandLine.NamingConventionBinder/PropertyDescriptor.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ internal PropertyDescriptor(
3636
public bool HasDefaultValue => false;
3737

3838
/// <inheritdoc />
39-
public object? GetDefaultValue() => ArgumentConverter.GetDefaultValue(ValueType);
39+
public object? GetDefaultValue() => ValueType.GetDefaultValue();
4040

4141
/// <summary>
4242
/// Sets a value on the target property.

src/System.CommandLine.NamingConventionBinder/TypeExtensions.cs

+27
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
// Copyright (c) .NET Foundation and contributors. All rights reserved.
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

4+
using System.Collections;
5+
using System.CommandLine.Binding;
46
using System.Runtime.CompilerServices;
7+
using System.Runtime.Serialization;
58

69
namespace System.CommandLine.NamingConventionBinder;
710

@@ -28,4 +31,28 @@ public static bool IsConstructedGenericTypeOf(this Type type, Type genericTypeDe
2831
[MethodImpl(MethodImplOptions.AggressiveInlining)]
2932
public static bool IsNullableValueType(this Type type)
3033
=> type.IsValueType && type.IsConstructedGenericTypeOf(typeof(Nullable<>));
34+
35+
internal static object? GetDefaultValue(this Type type)
36+
{
37+
if (type.IsNullable())
38+
{
39+
return null;
40+
}
41+
42+
if (type.GetElementTypeIfEnumerable() is { } itemType)
43+
{
44+
return ArgumentConverter.CreateEnumerable(type, itemType);
45+
}
46+
47+
return type switch
48+
{
49+
{ } nonGeneric
50+
when nonGeneric == typeof(IList) ||
51+
nonGeneric == typeof(ICollection) ||
52+
nonGeneric == typeof(IEnumerable)
53+
=> Array.Empty<object>(),
54+
_ when type.IsValueType => FormatterServices.GetUninitializedObject(type),
55+
_ => null
56+
};
57+
}
3158
}

src/System.CommandLine.Tests/Binding/TypeConversionTests.cs

+27-9
Original file line numberDiff line numberDiff line change
@@ -531,8 +531,15 @@ public void Values_can_be_correctly_converted_to_nullable_TimeSpan_without_the_p
531531
}
532532

533533
[Fact]
534-
public void Values_can_be_correctly_converted_to_Uri_without_the_parser_specifying_a_custom_converter()
535-
=> GetValue(new Option<Uri>("-x"), "-x http://example.com").Should().BeEquivalentTo(new Uri("http://example.com"));
534+
public void Values_can_be_correctly_converted_to_Uri_when_custom_parser_is_provided()
535+
{
536+
Option<Uri> option = new ("-x")
537+
{
538+
CustomParser = (argumentResult) => Uri.TryCreate(argumentResult.Tokens.Last().Value, UriKind.RelativeOrAbsolute, out var uri) ? uri : null
539+
};
540+
541+
GetValue(option, "-x http://example.com").Should().BeEquivalentTo(new Uri("http://example.com"));
542+
}
536543

537544
[Fact]
538545
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
582589
=> GetValue(new Option<sbyte?>("-us"), "-us 123").Should().Be(123);
583590

584591
[Fact]
585-
public void Values_can_be_correctly_converted_to_ipaddress_without_the_parser_specifying_a_custom_converter()
586-
=> GetValue(new Option<IPAddress>("-us"), "-us 1.2.3.4").Should().Be(IPAddress.Parse("1.2.3.4"));
592+
public void Values_can_be_correctly_converted_to_ipaddress_when_custom_parser_is_provided()
593+
{
594+
Option<IPAddress> option = new ("-us")
595+
{
596+
CustomParser = (argumentResult) => IPAddress.Parse(argumentResult.Tokens.Last().Value)
597+
};
598+
599+
GetValue(option, "-us 1.2.3.4").Should().Be(IPAddress.Parse("1.2.3.4"));
600+
}
587601

588602
#if NETCOREAPP3_0_OR_GREATER
589603
[Fact]
590-
public void Values_can_be_correctly_converted_to_ipendpoint_without_the_parser_specifying_a_custom_converter()
591-
=> GetValue(new Option<IPEndPoint>("-us"), "-us 1.2.3.4:56").Should().Be(IPEndPoint.Parse("1.2.3.4:56"));
604+
public void Values_can_be_correctly_converted_to_ipendpoint_when_custom_parser_is_provided()
605+
{
606+
Option<IPEndPoint> option = new("-us")
607+
{
608+
CustomParser = (argumentResult) => IPEndPoint.Parse(argumentResult.Tokens.Last().Value)
609+
};
610+
611+
GetValue(option, "-us 1.2.3.4:56").Should().Be(IPEndPoint.Parse("1.2.3.4:56"));
612+
}
592613
#endif
593614

594615
#if NET6_0_OR_GREATER
@@ -756,9 +777,6 @@ public void String_defaults_to_null_when_not_specified_only_for_not_required_arg
756777
[InlineData(typeof(string[]))]
757778
[InlineData(typeof(int[]))]
758779
[InlineData(typeof(FileAccess[]))]
759-
[InlineData(typeof(IEnumerable))]
760-
[InlineData(typeof(ICollection))]
761-
[InlineData(typeof(IList))]
762780
public void Sequence_type_defaults_to_empty_when_not_specified(Type sequenceType)
763781
{
764782
var argument = Activator.CreateInstance(typeof(Argument<>).MakeGenericType(sequenceType), new object[] { "argName" });

src/System.CommandLine/Argument.cs

+33-4
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,40 @@ internal TryConvertArgument? ConvertArguments
5454
/// <summary>
5555
/// Gets the list of completion sources for the argument.
5656
/// </summary>
57-
public List<Func<CompletionContext, IEnumerable<CompletionItem>>> CompletionSources =>
58-
_completionSources ??= new ()
57+
public List<Func<CompletionContext, IEnumerable<CompletionItem>>> CompletionSources
58+
{
59+
get
5960
{
60-
CompletionSource.ForType(ValueType)
61-
};
61+
if (_completionSources is null)
62+
{
63+
Type? valueType = ValueType;
64+
if (valueType == typeof(bool) || valueType == typeof(bool?))
65+
{
66+
_completionSources = new ()
67+
{
68+
static _ => new CompletionItem[]
69+
{
70+
new(bool.TrueString),
71+
new(bool.FalseString)
72+
}
73+
};
74+
}
75+
else if (!valueType.IsPrimitive && (valueType.IsEnum || (valueType.TryGetNullableType(out valueType) && valueType.IsEnum)))
76+
{
77+
_completionSources = new()
78+
{
79+
_ => Enum.GetNames(valueType).Select(n => new CompletionItem(n))
80+
};
81+
}
82+
else
83+
{
84+
_completionSources = new();
85+
}
86+
}
87+
88+
return _completionSources;
89+
}
90+
}
6291

6392
/// <summary>
6493
/// Gets or sets the <see cref="Type" /> that the argument token(s) will be converted to.

src/System.CommandLine/Argument{T}.cs

+33
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
// Copyright (c) .NET Foundation and contributors. All rights reserved.
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

4+
using System.Collections.Generic;
45
using System.CommandLine.Parsing;
6+
using System.Diagnostics.CodeAnalysis;
57
using System.IO;
68

79
namespace System.CommandLine
@@ -160,5 +162,36 @@ public void AcceptLegalFileNamesOnly()
160162
}
161163
});
162164
}
165+
166+
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050", Justification = "https://github.com/dotnet/command-line-api/issues/1638")]
167+
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2091", Justification = "https://github.com/dotnet/command-line-api/issues/1638")]
168+
internal static T? CreateDefaultValue()
169+
{
170+
if (default(T) is null && typeof(T) != typeof(string))
171+
{
172+
if (typeof(T).IsArray)
173+
{
174+
return (T?)(object)Array.CreateInstance(typeof(T).GetElementType()!, 0);
175+
}
176+
else if (typeof(T).IsGenericType)
177+
{
178+
var genericTypeDefinition = typeof(T).GetGenericTypeDefinition();
179+
180+
if (genericTypeDefinition == typeof(IEnumerable<>) ||
181+
genericTypeDefinition == typeof(IList<>) ||
182+
genericTypeDefinition == typeof(ICollection<>))
183+
{
184+
return (T?)(object)Array.CreateInstance(typeof(T).GenericTypeArguments[0], 0);
185+
}
186+
187+
if (genericTypeDefinition == typeof(List<>))
188+
{
189+
return Activator.CreateInstance<T>();
190+
}
191+
}
192+
}
193+
194+
return default;
195+
}
163196
}
164197
}

src/System.CommandLine/Binding/ArgumentConverter.DefaultValues.cs

+1-7
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using System.Collections.Generic;
66
using System.Diagnostics.CodeAnalysis;
77
using System.Reflection;
8-
using System.Runtime.Serialization;
98

109
namespace System.CommandLine.Binding;
1110

@@ -37,7 +36,7 @@ private static IList CreateEmptyList(Type listType)
3736
return (IList)ctor.Invoke(null);
3837
}
3938

40-
private static IList CreateEnumerable(Type type, Type itemType, int capacity = 0)
39+
internal static IList CreateEnumerable(Type type, Type itemType, int capacity = 0)
4140
{
4241
if (type.IsArray)
4342
{
@@ -63,9 +62,4 @@ private static IList CreateEnumerable(Type type, Type itemType, int capacity = 0
6362

6463
throw new ArgumentException($"Type {type} cannot be created without a custom binder.");
6564
}
66-
67-
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2067:UnrecognizedReflectionPattern",
68-
Justification = $"{nameof(CreateDefaultValueType)} is only called on a ValueType. You can always create an instance of a ValueType.")]
69-
private static object CreateDefaultValueType(Type type) =>
70-
FormatterServices.GetUninitializedObject(type);
7165
}

src/System.CommandLine/Binding/ArgumentConverter.StringConverters.cs

+4-40
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,17 @@
33

44
using System.Collections.Generic;
55
using System.IO;
6-
using System.Net;
76

87
namespace System.CommandLine.Binding;
98

109
internal static partial class ArgumentConverter
1110
{
1211
private delegate bool TryConvertString(string token, out object? value);
1312

14-
private static readonly Dictionary<Type, TryConvertString> _stringConverters = new()
13+
private static Dictionary<Type, TryConvertString>? _stringConverters;
14+
15+
private static Dictionary<Type, TryConvertString> StringConverters
16+
=> _stringConverters ??= new()
1517
{
1618
[typeof(bool)] = (string token, out object? value) =>
1719
{
@@ -169,32 +171,6 @@ internal static partial class ArgumentConverter
169171
return false;
170172
},
171173

172-
[typeof(IPAddress)] = (string token, out object? value) =>
173-
{
174-
if (IPAddress.TryParse(token, out var ip))
175-
{
176-
value = ip;
177-
return true;
178-
}
179-
180-
value = default;
181-
return false;
182-
},
183-
184-
#if NETCOREAPP3_0_OR_GREATER
185-
[typeof(IPEndPoint)] = (string token, out object? value) =>
186-
{
187-
if (IPEndPoint.TryParse(token, out var ipendpoint))
188-
{
189-
value = ipendpoint;
190-
return true;
191-
}
192-
193-
value = default;
194-
return false;
195-
},
196-
#endif
197-
198174
[typeof(long)] = (string token, out object? value) =>
199175
{
200176
if (long.TryParse(token, out var longValue))
@@ -299,18 +275,6 @@ internal static partial class ArgumentConverter
299275
return false;
300276
},
301277

302-
[typeof(Uri)] = (string input, out object? value) =>
303-
{
304-
if (Uri.TryCreate(input, UriKind.RelativeOrAbsolute, out var uri))
305-
{
306-
value = uri;
307-
return true;
308-
}
309-
310-
value = default;
311-
return false;
312-
},
313-
314278
[typeof(TimeSpan)] = (string input, out object? value) =>
315279
{
316280
if (TimeSpan.TryParse(input, out var timeSpan))

src/System.CommandLine/Binding/ArgumentConverter.cs

+3-28
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright (c) .NET Foundation and contributors. All rights reserved.
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

4-
using System.Collections;
54
using System.Collections.Generic;
65
using System.CommandLine.Parsing;
76
using static System.CommandLine.Binding.ArgumentConversionResult;
@@ -40,7 +39,7 @@ private static ArgumentConversionResult ConvertToken(
4039
return ConvertToken(argumentResult, nullableType, token);
4140
}
4241

43-
if (_stringConverters.TryGetValue(type, out var tryConvert))
42+
if (StringConverters.TryGetValue(type, out var tryConvert))
4443
{
4544
if (tryConvert(value, out var converted))
4645
{
@@ -123,12 +122,12 @@ private static ArgumentConversionResult ConvertTokens(
123122
if (argument.Arity is { MaximumNumberOfValues: 1, MinimumNumberOfValues: 1 })
124123
{
125124
if (argument.ValueType.TryGetNullableType(out var nullableType) &&
126-
_stringConverters.TryGetValue(nullableType, out var convertNullable))
125+
StringConverters.TryGetValue(nullableType, out var convertNullable))
127126
{
128127
return (ArgumentResult result, out object? value) => ConvertSingleString(result, convertNullable, out value);
129128
}
130129

131-
if (_stringConverters.TryGetValue(argument.ValueType, out var convert1))
130+
if (StringConverters.TryGetValue(argument.ValueType, out var convert1))
132131
{
133132
return (ArgumentResult result, out object? value) => ConvertSingleString(result, convert1, out value);
134133
}
@@ -224,29 +223,5 @@ public static bool TryConvertArgument(ArgumentResult argumentResult, out object?
224223
value = result;
225224
return result.Result == ArgumentConversionResultType.Successful;
226225
}
227-
228-
internal static object? GetDefaultValue(Type type)
229-
{
230-
if (type.IsNullable())
231-
{
232-
return null;
233-
}
234-
235-
if (type.GetElementTypeIfEnumerable() is { } itemType)
236-
{
237-
return CreateEnumerable(type, itemType);
238-
}
239-
240-
return type switch
241-
{
242-
{ } nonGeneric
243-
when nonGeneric == typeof(IList) ||
244-
nonGeneric == typeof(ICollection) ||
245-
nonGeneric == typeof(IEnumerable)
246-
=> Array.Empty<object>(),
247-
_ when type.IsValueType => CreateDefaultValueType(type),
248-
_ => null
249-
};
250-
}
251226
}
252227
}

0 commit comments

Comments
 (0)