Skip to content

Commit 9097545

Browse files
committed
fix #1195 & #1188, R: move some methods from ArgumentConverter to Binder
1 parent 465f9e5 commit 9097545

14 files changed

+224
-194
lines changed

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
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.Binding;
56
using System.CommandLine.Builder;
67
using System.CommandLine.Invocation;
@@ -625,5 +626,39 @@ public void Default_values_from_options_with_the_same_type_are_bound_and_use_the
625626
first.Should().Be(1);
626627
second.Should().Be(2);
627628
}
629+
630+
public class MyModel
631+
{
632+
public List<string> Abc { get; set; }
633+
634+
public string AbcDef { get; set; }
635+
}
636+
637+
[Fact]
638+
public void Binder_does_not_match_on_partial_name()
639+
{
640+
var command = new RootCommand
641+
{
642+
new Option<List<string>>("--abc")
643+
};
644+
645+
MyModel boundValue = default;
646+
647+
command.Handler = CommandHandler.Create(
648+
(MyModel s) =>
649+
{
650+
boundValue = s;
651+
}
652+
);
653+
654+
command.Invoke(new[] { "--abc", "1" });
655+
656+
boundValue.Abc
657+
.Should()
658+
.ContainSingle()
659+
.Which
660+
.Should()
661+
.Be("1");
662+
}
628663
}
629664
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ await handler.InvokeAsync(
173173
}
174174

175175
[Theory]
176-
[InlineData(typeof(string), null)]
176+
[InlineData(typeof(string), "")]
177177
[InlineData(typeof(FileInfo), null)]
178178
[InlineData(typeof(int), 0)]
179179
[InlineData(typeof(int?), null)]
@@ -365,7 +365,7 @@ public async Task When_binding_fails_due_to_parameter_naming_mismatch_then_handl
365365

366366
testConsole.Error.ToString().Should().BeEmpty();
367367

368-
received.Should().BeNull();
368+
received.Should().BeEmpty();
369369
}
370370

371371
[Theory]

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace System.CommandLine.Tests.Binding
1111
public class ParameterDescriptorTests
1212
{
1313
[Theory]
14-
[InlineData(typeof(string), null)]
14+
[InlineData(typeof(string), "")]
1515
[InlineData(typeof(int), 0)]
1616
[InlineData(typeof(int?), null)]
1717
public void GetDefaultValue_returns_the_default_for_the_type(Type type, object defaultValue)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace System.CommandLine.Tests.Binding
1111
public class PropertyDescriptorTests
1212
{
1313
[Theory]
14-
[InlineData(typeof(string), null)]
14+
[InlineData(typeof(string), "")]
1515
[InlineData(typeof(int), 0)]
1616
[InlineData(typeof(int?), null)]
1717
public void GetDefaultValue_returns_the_default_for_the_type(Type type, object defaultValue)

src/System.CommandLine.Tests/Invocation/CommandHandlerTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ void Execute(string name, int age)
142142

143143
await command.InvokeAsync("command", _console);
144144

145-
boundName.Should().Be(null);
145+
boundName.Should().Be("");
146146
boundAge.Should().Be(0);
147147
}
148148

src/System.CommandLine.Tests/OptionTests.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,21 @@ public void When_aliases_overlap_the_longer_alias_is_chosen(string parseInput)
460460
parseResult.ValueForOption(option).Should().Be("value");
461461
}
462462

463+
[Fact]
464+
public void Option_of_boolean_defaults_to_false_when_not_specified()
465+
{
466+
var option = new Option<bool>("-x");
467+
468+
var result = option.Parse("");
469+
470+
result.HasOption(option)
471+
.Should()
472+
.BeFalse();
473+
result.ValueForOption(option)
474+
.Should()
475+
.BeFalse();
476+
}
477+
463478
protected override Symbol CreateSymbol(string name) => new Option(name);
464479
}
465480
}

src/System.CommandLine/Binding/ArgumentConverter.cs

Lines changed: 4 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ public static ArgumentConversionResult ConvertStrings(
111111
{
112112
var itemType = type == typeof(string)
113113
? typeof(string)
114-
: GetItemTypeIfEnumerable(type);
114+
: Binder.GetItemTypeIfEnumerable(type);
115115

116116
var (values, isArray) = type.IsArray
117117
? (CreateArray(itemType!, tokens.Count), true)
@@ -181,45 +181,9 @@ static IList CreateArray(Type itemType, int capacity)
181181
}
182182
}
183183

184-
private static Type? GetItemTypeIfEnumerable(Type type)
185-
{
186-
if (type.IsArray)
187-
{
188-
return type.GetElementType();
189-
}
190-
191-
var enumerableInterface =
192-
IsEnumerable(type)
193-
? type
194-
: type
195-
.GetInterfaces()
196-
.FirstOrDefault(IsEnumerable);
197-
198-
return enumerableInterface?.GenericTypeArguments switch
199-
{
200-
{ Length: 1 } genericTypeArguments => genericTypeArguments[0],
201-
_ => null
202-
};
203-
}
204-
205-
internal static bool IsEnumerable(this Type type)
206-
{
207-
if (type == typeof(string))
208-
{
209-
return false;
210-
}
211-
212-
return
213-
type.IsArray
214-
||
215-
typeof(IEnumerable).IsAssignableFrom(type);
216-
}
217-
218-
private static bool HasStringTypeConverter(this Type type)
219-
{
220-
return TypeDescriptor.GetConverter(type) is { } typeConverter
221-
&& typeConverter.CanConvertFrom(typeof(string));
222-
}
184+
internal static bool HasStringTypeConverter(this Type type) =>
185+
TypeDescriptor.GetConverter(type) is { } typeConverter
186+
&& typeConverter.CanConvertFrom(typeof(string));
223187

224188
private static FailedArgumentConversionResult Failure(
225189
IArgument argument,
@@ -229,61 +193,6 @@ private static FailedArgumentConversionResult Failure(
229193
return new FailedArgumentTypeConversionResult(argument, expectedType, value);
230194
}
231195

232-
public static bool CanBeBoundFromScalarValue(this Type type)
233-
{
234-
if (type.IsPrimitive ||
235-
type.IsEnum)
236-
{
237-
return true;
238-
}
239-
240-
if (type == typeof(string))
241-
{
242-
return true;
243-
}
244-
245-
if (TypeDescriptor.GetConverter(type) is { } typeConverter &&
246-
typeConverter.CanConvertFrom(typeof(string)))
247-
{
248-
return true;
249-
}
250-
251-
if (TryFindConstructorWithSingleParameterOfType(type, typeof(string), out _))
252-
{
253-
return true;
254-
}
255-
256-
if (GetItemTypeIfEnumerable(type) is { } itemType)
257-
{
258-
return itemType.CanBeBoundFromScalarValue();
259-
}
260-
261-
return false;
262-
}
263-
264-
private static bool TryFindConstructorWithSingleParameterOfType(
265-
this Type type,
266-
Type parameterType,
267-
[NotNullWhen(true)] out ConstructorInfo? ctor)
268-
{
269-
var (x, _) = type.GetConstructors()
270-
.Select(c => (ctor: c, parameters: c.GetParameters()))
271-
.SingleOrDefault(tuple => tuple.ctor.IsPublic &&
272-
tuple.parameters.Length == 1 &&
273-
tuple.parameters[0].ParameterType == parameterType);
274-
275-
if (x != null)
276-
{
277-
ctor = x;
278-
return true;
279-
}
280-
else
281-
{
282-
ctor = null;
283-
return false;
284-
}
285-
}
286-
287196
internal static ArgumentConversionResult ConvertIfNeeded(
288197
this ArgumentConversionResult conversionResult,
289198
SymbolResult symbolResult,
@@ -328,9 +237,6 @@ internal static ArgumentConversionResult ConvertIfNeeded(
328237
}
329238
}
330239

331-
internal static object? GetValueOrDefault(this ArgumentConversionResult result) =>
332-
result.GetValueOrDefault<object?>();
333-
334240
[return: MaybeNull]
335241
internal static T GetValueOrDefault<T>(this ArgumentConversionResult result)
336242
{
@@ -343,61 +249,6 @@ internal static T GetValueOrDefault<T>(this ArgumentConversionResult result)
343249
};
344250
}
345251

346-
[return: MaybeNull]
347-
internal static T GetDefaultValue<T>()
348-
{
349-
return (T)GetDefaultValue(typeof(T));
350-
}
351-
352-
private static MethodInfo EnumerableEmptyMethod { get; }
353-
= typeof(Enumerable).GetMethod(nameof(Enumerable.Empty));
354-
355-
internal static object? GetDefaultValue(Type type)
356-
{
357-
if (type == typeof(string)) return "";
358-
if (GetItemTypeIfEnumerable(type) is Type itemType)
359-
{
360-
if (type.IsArray)
361-
{
362-
return CreateEmptyArray(itemType);
363-
}
364-
if (type.IsGenericType)
365-
{
366-
return type.GetGenericTypeDefinition() switch
367-
{
368-
Type enumerable when enumerable == typeof(IEnumerable<>) => GetEmptyEnumerable(itemType),
369-
Type list when list == typeof(List<>) => GetEmptyList(itemType),
370-
Type array when array == typeof(IList<>) ||
371-
array == typeof(ICollection<>) => CreateEmptyArray(itemType),
372-
_ => null
373-
};
374-
}
375-
}
376-
return type switch
377-
{
378-
Type nonGeneric
379-
when nonGeneric == typeof(IList) ||
380-
nonGeneric == typeof(ICollection) ||
381-
nonGeneric == typeof(IEnumerable)
382-
=> CreateEmptyArray(typeof(object)),
383-
_ => null
384-
};
385-
386-
static object GetEmptyList(Type itemType)
387-
{
388-
return Activator.CreateInstance(typeof(List<>).MakeGenericType(itemType));
389-
}
390-
391-
static IEnumerable GetEmptyEnumerable(Type itemType)
392-
{
393-
var genericMethod = EnumerableEmptyMethod.MakeGenericMethod(itemType);
394-
return (IEnumerable)genericMethod.Invoke(null, new object[0]);
395-
}
396-
397-
static Array CreateEmptyArray(Type itemType)
398-
=> Array.CreateInstance(itemType, 0);
399-
}
400-
401252
public static bool TryConvertBoolArgument(ArgumentResult argumentResult, out object? value)
402253
{
403254
if (argumentResult.Tokens.Count == 0)

0 commit comments

Comments
 (0)