diff --git a/src/CommandLine/CommandLine.csproj b/src/CommandLine/CommandLine.csproj index 04496eb8..ea29f672 100644 --- a/src/CommandLine/CommandLine.csproj +++ b/src/CommandLine/CommandLine.csproj @@ -3,7 +3,7 @@ CommandLine Library - netstandard2.0;net40;net45;net461 + netstandard2.0;net40;net45;net461;net5.0 $(DefineConstants);CSX_EITHER_INTERNAL;CSX_REM_EITHER_BEYOND_2;CSX_ENUM_INTERNAL;ERRH_INTERNAL;CSX_MAYBE_INTERNAL;CSX_REM_EITHER_FUNC;CSX_REM_CRYPTORAND;ERRH_ADD_MAYBE_METHODS $(DefineConstants);SKIP_FSHARP true diff --git a/src/CommandLine/Core/ReflectionExtensions.cs b/src/CommandLine/Core/ReflectionExtensions.cs index 622e1e6e..d050c111 100644 --- a/src/CommandLine/Core/ReflectionExtensions.cs +++ b/src/CommandLine/Core/ReflectionExtensions.cs @@ -39,9 +39,9 @@ public static Maybe> GetUsageData(this Type { return (from pi in type.FlattenHierarchy().SelectMany(x => x.GetTypeInfo().GetProperties()) - let attrs = pi.GetCustomAttributes(typeof(UsageAttribute), true) - where attrs.Any() - select Tuple.Create(pi, (UsageAttribute)attrs.First())) + let attrs = pi.GetCustomAttributes(typeof(UsageAttribute), true) + where attrs.Any() + select Tuple.Create(pi, (UsageAttribute)attrs.First())) .SingleOrDefault() .ToMaybe(); } @@ -125,16 +125,16 @@ public static object GetDefaultValue(this Type type) public static bool IsMutable(this Type type) { - if(type == typeof(object)) + if (type == typeof(object)) return true; // Find all inherited defined properties and fields on the type var inheritedTypes = type.GetTypeInfo().FlattenHierarchy().Select(i => i.GetTypeInfo()); - foreach (var inheritedType in inheritedTypes) + foreach (var inheritedType in inheritedTypes) { if ( - inheritedType.GetTypeInfo().GetProperties(BindingFlags.Public | BindingFlags.Instance).Any(p => p.CanWrite) || + inheritedType.GetTypeInfo().GetProperties(BindingFlags.Public | BindingFlags.Instance).Any(p => p.IsMutable()) || inheritedType.GetTypeInfo().GetFields(BindingFlags.Public | BindingFlags.Instance).Any() ) { @@ -162,7 +162,7 @@ public static object AutoDefault(this Type type) } var ctorTypes = type.GetSpecifications(pi => pi.PropertyType).ToArray(); - + return ReflectionHelper.CreateDefaultImmutableInstance(type, ctorTypes); } @@ -206,7 +206,7 @@ public static bool IsPrimitiveEx(this Type type) return (type.GetTypeInfo().IsValueType && type != typeof(Guid)) || type.GetTypeInfo().IsPrimitive - || new [] { + || new[] { typeof(string) ,typeof(decimal) ,typeof(DateTime) @@ -218,10 +218,31 @@ public static bool IsPrimitiveEx(this Type type) public static bool IsCustomStruct(this Type type) { - var isStruct = type.GetTypeInfo().IsValueType && !type.GetTypeInfo().IsPrimitive && !type.GetTypeInfo().IsEnum && type != typeof(Guid); + var isStruct = type.GetTypeInfo().IsValueType && !type.GetTypeInfo().IsPrimitive && !type.GetTypeInfo().IsEnum && type != typeof(Guid); if (!isStruct) return false; var ctor = type.GetTypeInfo().GetConstructor(new[] { typeof(string) }); return ctor != null; } + +#if NET5_0_OR_GREATER + public static bool IsInitOnly(this PropertyInfo propertyInfo) + { + var setMethod = propertyInfo.SetMethod; + + if (setMethod == null) + return false; + + var isExternalInitType = typeof(System.Runtime.CompilerServices.IsExternalInit); + + return setMethod.ReturnParameter.GetRequiredCustomModifiers().Contains(isExternalInitType); + } +#endif + + public static bool IsMutable(this PropertyInfo propertyInfo) => +#if NET5_0_OR_GREATER + propertyInfo.CanWrite && propertyInfo.GetSetMethod(false) != null && !propertyInfo.IsInitOnly(); +#else + propertyInfo.CanWrite && propertyInfo.GetSetMethod(false) != null; +#endif } } diff --git a/src/CommandLine/Core/TokenPartitioner.cs b/src/CommandLine/Core/TokenPartitioner.cs index 4dc25f7f..2220b0db 100644 --- a/src/CommandLine/Core/TokenPartitioner.cs +++ b/src/CommandLine/Core/TokenPartitioner.cs @@ -5,6 +5,7 @@ using System.Linq; using CommandLine.Infrastructure; using CSharpx; +using ReferenceEqualityComparer = CommandLine.Infrastructure.ReferenceEqualityComparer; namespace CommandLine.Core { diff --git a/tests/CommandLine.Tests/CommandLine.Tests.csproj b/tests/CommandLine.Tests/CommandLine.Tests.csproj index d4dbcab0..5340b1ef 100644 --- a/tests/CommandLine.Tests/CommandLine.Tests.csproj +++ b/tests/CommandLine.Tests/CommandLine.Tests.csproj @@ -2,7 +2,7 @@ Library - net461;netcoreapp3.1 + net461;netcoreapp3.1;net5.0 $(DefineConstants);SKIP_FSHARP ..\..\CommandLine.snk true @@ -35,4 +35,4 @@ - \ No newline at end of file + diff --git a/tests/CommandLine.Tests/Fakes/Immutable_Simple_Options.cs b/tests/CommandLine.Tests/Fakes/Immutable_Simple_Options.cs index ae829e7d..8cd29ce8 100644 --- a/tests/CommandLine.Tests/Fakes/Immutable_Simple_Options.cs +++ b/tests/CommandLine.Tests/Fakes/Immutable_Simple_Options.cs @@ -31,7 +31,7 @@ public Immutable_Simple_Options(string stringValue, IEnumerable intSequence [Value(0)] public long LongValue { get { return longValue; } } } - + public class Immutable_Simple_Options_Invalid_Ctor_Args { private readonly string stringValue; @@ -59,4 +59,89 @@ public Immutable_Simple_Options_Invalid_Ctor_Args(string stringValue1, IEnumerab [Value(0)] public long LongValue { get { return longValue; } } } + + public class Immutable_Simple_Options_Read_Only + { + public Immutable_Simple_Options_Read_Only(string stringValue, IEnumerable intSequence, bool boolValue, long longValue) + { + StringValue = stringValue; + IntSequence = intSequence; + BoolValue = boolValue; + LongValue = longValue; + } + + [Option(HelpText = "Define a string value here.")] + public string StringValue { get; } + + [Option('i', Min = 3, Max = 4, HelpText = "Define a int sequence here.")] + public IEnumerable IntSequence { get; } + + [Option('x', HelpText = "Define a boolean or switch value here.")] + public bool BoolValue { get; } + + [Value(0)] + public long LongValue { get; } + } + + public class Immutable_Simple_Options_Private_Set + { + public Immutable_Simple_Options_Private_Set(string stringValue, IEnumerable intSequence, bool boolValue, long longValue) + { + StringValue = stringValue; + IntSequence = intSequence; + BoolValue = boolValue; + LongValue = longValue; + } + + [Option(HelpText = "Define a string value here.")] + public string StringValue { get; private set; } + + [Option('i', Min = 3, Max = 4, HelpText = "Define a int sequence here.")] + public IEnumerable IntSequence { get; private set; } + + [Option('x', HelpText = "Define a boolean or switch value here.")] + public bool BoolValue { get; private set; } + + [Value(0)] + public long LongValue { get; private set; } + } + +#if NET5_0_OR_GREATER + public class Immutable_Simple_Options_Init + { + public Immutable_Simple_Options_Init(string stringValue, IEnumerable intSequence, bool boolValue, long longValue) + { + StringValue = stringValue; + IntSequence = intSequence; + BoolValue = boolValue; + LongValue = longValue; + } + + [Option(HelpText = "Define a string value here.")] + public string StringValue { get; init; } + + [Option('i', Min = 3, Max = 4, HelpText = "Define a int sequence here.")] + public IEnumerable IntSequence { get; init; } + + [Option('x', HelpText = "Define a boolean or switch value here.")] + public bool BoolValue { get; init; } + + [Value(0)] + public long LongValue { get; init; } + } + + public record Immutable_Simple_Options_Record( + [property: Option(HelpText = "Define a string value here.")] + string StringValue, + + [property: Option('i', Min = 3, Max = 4, HelpText = "Define a int sequence here.")] + IEnumerable IntSequence, + + [property: Option('x', HelpText = "Define a boolean or switch value here.")] + bool BoolValue, + + [property: Value(0)] + long LongValue + ); +#endif } diff --git a/tests/CommandLine.Tests/Unit/Core/ReflectionExtensions.cs b/tests/CommandLine.Tests/Unit/Core/ReflectionExtensions.cs index 035b9e03..6182947d 100644 --- a/tests/CommandLine.Tests/Unit/Core/ReflectionExtensions.cs +++ b/tests/CommandLine.Tests/Unit/Core/ReflectionExtensions.cs @@ -5,7 +5,7 @@ using CommandLine.Core; using CommandLine.Tests.Fakes; -namespace CommandLine.Tests.Unit.Infrastructure +namespace CommandLine.Tests.Unit.Core { public class ReflectionHelperTests { @@ -20,5 +20,31 @@ public static void Class_without_public_set_properties_or_fields_is_ranked_immut { typeof(Immutable_Simple_Options).IsMutable().Should().BeFalse(); } + + [Fact] + public static void Class_with_read_only_properties_is_ranked_immutable() + { + typeof(Immutable_Simple_Options_Read_Only).IsMutable().Should().BeFalse(); + } + + [Fact] + public static void Class_with_private_set_properties_is_ranked_immutable() + { + typeof(Immutable_Simple_Options_Private_Set).IsMutable().Should().BeFalse(); + } + +#if NET5_0_OR_GREATER + [Fact] + public static void Class_with_init_properties_is_ranked_immutable() + { + typeof(Immutable_Simple_Options_Init).IsMutable().Should().BeFalse(); + } + + [Fact] + public static void Record_without_public_set_properties_or_fields_is_ranked_immutable() + { + typeof(Immutable_Simple_Options_Record).IsMutable().Should().BeFalse(); + } +#endif } } diff --git a/tests/CommandLine.Tests/Unit/Issue777Tests.cs b/tests/CommandLine.Tests/Unit/Issue777Tests.cs new file mode 100644 index 00000000..1d27816b --- /dev/null +++ b/tests/CommandLine.Tests/Unit/Issue777Tests.cs @@ -0,0 +1,42 @@ +using FluentAssertions; +using Xunit; + +// Issue #777 +// Record types should be usable as immutable options types. + +namespace CommandLine.Tests.Unit +{ +#if NET5_0_OR_GREATER + public class Issue777Tests + { + [Fact] + public void Immutable_record_types_should_work() + { + var arguments = new[] { "--option1=test", "--option2=5", "--option3" }; + var result = Parser.Default + .ParseArguments(arguments); + + Assert.Empty(result.Errors); + Assert.Equal(ParserResultType.Parsed, result.Tag); + + result.WithParsed(options => + { + options.Option1.Should().Be("test"); + options.Option2.Should().Be(5); + options.Option3.Should().Be(true); + }); + } + + private record Options( + [property: Option] + string Option1, + + [property: Option] + int Option2, + + [property: Option] + bool Option3 + ); + } +#endif +}