From 1a291c969a0cc0de24c437cf63b46d1ed3faddab Mon Sep 17 00:00:00 2001 From: Alexey Evlampiev Date: Sun, 16 Feb 2020 12:34:06 +0100 Subject: [PATCH] Fix to the issue "System.InvalidOperationException: on parsing GUID command line option #333" --- .../Abstractions/DefaultValueParserFactory.cs | 123 ++++++++++++++++++ .../Abstractions/ValueParserProvider.cs | 4 + .../ValueParserProviderTests.cs | 35 +++++ 3 files changed, 162 insertions(+) create mode 100644 src/CommandLineUtils/Abstractions/DefaultValueParserFactory.cs diff --git a/src/CommandLineUtils/Abstractions/DefaultValueParserFactory.cs b/src/CommandLineUtils/Abstractions/DefaultValueParserFactory.cs new file mode 100644 index 00000000..41a75eda --- /dev/null +++ b/src/CommandLineUtils/Abstractions/DefaultValueParserFactory.cs @@ -0,0 +1,123 @@ +namespace McMaster.Extensions.CommandLineUtils.Abstractions +{ + using System; + using System.Collections.Generic; + using System.ComponentModel; + using System.Diagnostics; + using System.Globalization; + using System.Text; + + /// + /// A factory creating generic implementations of . The implementations are based + /// on automatically located classes that are suitable for parsing. + /// + sealed class DefaultValueParserFactory + { + const int DefaultMaxCacheCapacity = 100; + + #region Private Fields + + private readonly int _maxCacheCapacity; + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + readonly Dictionary _parsersByTargetType = new Dictionary(); + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + readonly HashSet _notSupportedTypes = new HashSet(); + + #endregion + + public DefaultValueParserFactory(int maxCacheCapacity = DefaultMaxCacheCapacity) + { + _maxCacheCapacity = maxCacheCapacity; + if (_maxCacheCapacity < 1) throw new ArgumentOutOfRangeException(nameof(maxCacheCapacity)); + } + + [DebuggerStepThrough] + public bool TryGetParser(out IValueParser parser) + { + if (TryGetParser(out IValueParser generalizedParser)) + { + parser = (IValueParser)generalizedParser; + return true; + } + + parser = null; + return false; + } + + public bool TryGetParser(out IValueParser parser) + { + var targetType = typeof(T); + if (_notSupportedTypes.Contains(targetType)) + { + parser = null; + return false; + } + + if (_parsersByTargetType.TryGetValue(targetType, out parser)) + { + Debug.Assert(targetType == parser.TargetType); + return true; + } + + var converter = TypeDescriptor.GetConverter(targetType); + if (converter.CanConvertFrom(typeof(string))) + { + if (_parsersByTargetType.Count >= _maxCacheCapacity) + _parsersByTargetType.Clear(); + parser = new TypeConverterValueParser(targetType, converter); + _parsersByTargetType[targetType] = parser; + Debug.Assert(_parsersByTargetType.Count <= _maxCacheCapacity); + return true; + } + + parser = null; + if (_notSupportedTypes.Count > _maxCacheCapacity) + _notSupportedTypes.Clear(); + _notSupportedTypes.Add(targetType); + Debug.Assert(_notSupportedTypes.Count <= _maxCacheCapacity); + return false; + } + + public IValueParser GetParser() + { + return TryGetParser(out IValueParser converter) + ? converter + : throw new NotSupportedException( + new StringBuilder($"No suitable type converter found for {typeof(T)}.") + .Append($" Make sure a type converter capable of parsing {typeof(string)} to {typeof(T)} exists and is discoverable.") + .Append($" Did you forget to annotate the target type with {typeof(TypeConverterAttribute)}?") + .ToString()); + } + + private sealed class TypeConverterValueParser : IValueParser + { + public TypeConverterValueParser(Type targetType, TypeConverter typeConverter) + { + TargetType = targetType ?? throw new ArgumentNullException(nameof(targetType)); + TypeConverter = typeConverter ?? throw new ArgumentNullException(nameof(typeConverter)); + } + + public Type TargetType { get; } + + private TypeConverter TypeConverter { get; } + + public T Parse(string argName, string value, CultureInfo culture) + { + try + { + culture ??= CultureInfo.InvariantCulture; + return (T)TypeConverter.ConvertFromString(null, culture, value); + } + catch (ArgumentException e) + { + throw new FormatException(e.Message, e); + } + } + + object IValueParser.Parse(string argName, string value, CultureInfo culture) => Parse(argName, value, culture); + } + + } +} diff --git a/src/CommandLineUtils/Abstractions/ValueParserProvider.cs b/src/CommandLineUtils/Abstractions/ValueParserProvider.cs index 5e5e2e1d..4206d6e7 100644 --- a/src/CommandLineUtils/Abstractions/ValueParserProvider.cs +++ b/src/CommandLineUtils/Abstractions/ValueParserProvider.cs @@ -15,6 +15,7 @@ namespace McMaster.Extensions.CommandLineUtils.Abstractions public class ValueParserProvider { private readonly Dictionary _parsers = new Dictionary(10); + private readonly DefaultValueParserFactory _defaultValueParserFactory = new DefaultValueParserFactory(); internal ValueParserProvider() { @@ -100,6 +101,9 @@ public IValueParser GetParser(Type type) return EnumParser.Create(type); } + if (_defaultValueParserFactory.TryGetParser(out parser)) + return parser; + if (ReflectionHelper.IsNullableType(type, out var wrappedType) && wrappedType != null) { if (wrappedType.IsEnum) diff --git a/test/CommandLineUtils.Tests/ValueParserProviderTests.cs b/test/CommandLineUtils.Tests/ValueParserProviderTests.cs index b48676ae..c21e2241 100644 --- a/test/CommandLineUtils.Tests/ValueParserProviderTests.cs +++ b/test/CommandLineUtils.Tests/ValueParserProviderTests.cs @@ -110,6 +110,12 @@ private class Program [Option("--timespan")] public TimeSpan TimeSpan { get; } + + [Option("--guid", CommandOptionType.SingleValue)] + public Guid Guid { get; } + + [Option("--guid-opt", CommandOptionType.SingleValue)] + public Guid? GuidOpt { get; } } private sealed class InCulture : IDisposable @@ -480,6 +486,35 @@ public void ParsesBoolArray(int repeat) Assert.All(parsed.Flags, value => Assert.True(value)); } + [Theory] + [InlineData("ff23ef12-500a-48df-9a5d-151c2adc2a0a")] + [InlineData("ff23ef12500a48df9a5d151c2adc2a0a")] + [InlineData("{ff23ef12-500a-48df-9a5d-151c2adc2a0a}")] + [InlineData("(ff23ef12-500a-48df-9a5d-151c2adc2a0a)")] + [InlineData("{0xff23ef12,0x500a,0x48df,{0x9a,0x5d,0x15,0x1c,0x2a,0xdc,0x2a,0x0a}}")] + public void ParsesGuid(string arg) + { + var expected = Guid.Parse("ff23ef12-500a-48df-9a5d-151c2adc2a0a"); + var parsed = CommandLineParser.ParseArgs("--guid", arg); + Assert.Equal(expected, parsed.Guid); + } + + [Theory] + [InlineData("ff23ef12-500a-48df-9a5d-151c2adc2a0a")] + [InlineData("ff23ef12500a48df9a5d151c2adc2a0a")] + [InlineData("{ff23ef12-500a-48df-9a5d-151c2adc2a0a}")] + [InlineData("(ff23ef12-500a-48df-9a5d-151c2adc2a0a)")] + [InlineData("{0xff23ef12,0x500a,0x48df,{0x9a,0x5d,0x15,0x1c,0x2a,0xdc,0x2a,0x0a}}")] + [InlineData("")] + public void ParsesGuidNullable(string arg) + { + var expected = String.IsNullOrWhiteSpace(arg) + ? (Guid?)null + : Guid.Parse("ff23ef12-500a-48df-9a5d-151c2adc2a0a"); + var parsed = CommandLineParser.ParseArgs("--guid-opt", arg); + Assert.Equal(expected, parsed.GuidOpt); + } + [Theory] [InlineData(nameof(Program.Float), "--float", "123.456,7", "de-DE", 123456.7f)] [InlineData(nameof(Program.Double), "--double", "123.456,789", "de-DE", 123456.789)]