diff --git a/CHANGELOG.md b/CHANGELOG.md index 1781e8ea..8f203488 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,9 @@ ### Fixes * [@claude]: Validate dotnet path exists before returning from `TryFindDotNetExePath` ([#600]) +* [@claude]: Fix nullable type lookup bypassing custom value parsers registered via `AddOrReplace` ([#559]) +[#559]: https://github.com/natemcmaster/CommandLineUtils/issues/559 [#560]: https://github.com/natemcmaster/CommandLineUtils/pull/560 [#600]: https://github.com/natemcmaster/CommandLineUtils/issues/600 diff --git a/src/CommandLineUtils/Abstractions/ValueParserProvider.cs b/src/CommandLineUtils/Abstractions/ValueParserProvider.cs index 89d730a7..12e96000 100644 --- a/src/CommandLineUtils/Abstractions/ValueParserProvider.cs +++ b/src/CommandLineUtils/Abstractions/ValueParserProvider.cs @@ -100,11 +100,6 @@ public IValueParser GetParser(Type type) return EnumParser.Create(type); } - if (_defaultValueParserFactory.TryGetParser(out parser)) - { - return parser; - } - if (ReflectionHelper.IsNullableType(type, out var wrappedType)) { if (wrappedType.IsEnum) @@ -118,6 +113,11 @@ public IValueParser GetParser(Type type) } } + if (_defaultValueParserFactory.TryGetParser(out parser)) + { + return parser; + } + if (ReflectionHelper.IsSpecialValueTupleType(type, out var wrappedType2)) { var innerParser = GetParser(wrappedType2); diff --git a/src/CommandLineUtils/releasenotes.props b/src/CommandLineUtils/releasenotes.props index 60678d41..9d29add0 100644 --- a/src/CommandLineUtils/releasenotes.props +++ b/src/CommandLineUtils/releasenotes.props @@ -8,6 +8,7 @@ Features: Fixes: * @claude: Validate dotnet path exists before returning from TryFindDotNetExePath (#600) +* @claude: Fix nullable type lookup bypassing custom value parsers registered via AddOrReplace (#559) Changes since 4.1: diff --git a/test/CommandLineUtils.Tests/ValueParserProviderCustomTests.cs b/test/CommandLineUtils.Tests/ValueParserProviderCustomTests.cs index a5295fae..dcc221c7 100644 --- a/test/CommandLineUtils.Tests/ValueParserProviderCustomTests.cs +++ b/test/CommandLineUtils.Tests/ValueParserProviderCustomTests.cs @@ -339,5 +339,71 @@ public void AddOrReplaceThrowsIfNullparser() Assert.Contains("parser", ex.Message); } + + private class CustomTimeSpanParser : IValueParser + { + public Type TargetType => typeof(TimeSpan); + + public TimeSpan Parse(string? argName, string? value, CultureInfo culture) + { + if (value != null && value.EndsWith("s")) + { + var seconds = int.Parse(value.Substring(0, value.Length - 1), culture); + return TimeSpan.FromSeconds(seconds); + } + + return TimeSpan.Parse(value!, culture); + } + + object? IValueParser.Parse(string? argName, string? value, CultureInfo culture) + => Parse(argName, value, culture); + } + + private class NullableTimeSpanOptionProgram + { + [Option("--timeout", CommandOptionType.SingleValue)] + public TimeSpan? Timeout { get; set; } + } + + private class NonNullableTimeSpanOptionProgram + { + [Option("--timeout", CommandOptionType.SingleValue)] + public TimeSpan Timeout { get; set; } + } + + [Fact] + public void CustomParserWorksForNullableBuiltInType() + { + var app = new CommandLineApplication(); + app.ValueParsers.AddOrReplace(new CustomTimeSpanParser()); + app.Conventions.UseDefaultConventions(); + + app.Parse("--timeout", "15s"); + + Assert.Equal(TimeSpan.FromSeconds(15), app.Model.Timeout); + } + + [Fact] + public void CustomParserWorksForNonNullableBuiltInType() + { + var app = new CommandLineApplication(); + app.ValueParsers.AddOrReplace(new CustomTimeSpanParser()); + app.Conventions.UseDefaultConventions(); + + app.Parse("--timeout", "15s"); + + Assert.Equal(TimeSpan.FromSeconds(15), app.Model.Timeout); + } + + [Fact] + public void NullableBuiltInTypeUsesDefaultParserWhenNoCustomParser() + { + var app = new CommandLineApplication(); + app.Conventions.UseDefaultConventions(); + + app.Parse("--timeout", "00:00:15"); + + Assert.Equal(TimeSpan.FromSeconds(15), app.Model.Timeout); + } } }