diff --git a/.gitignore b/.gitignore index 55f900a1..7cccb220 100644 --- a/.gitignore +++ b/.gitignore @@ -37,5 +37,6 @@ artifacts/* *.DotSettings.user # Visual Studio 2015 cache/options directory .vs/ +.idea/ [R|r]elease/** diff --git a/src/CommandLine/CastExtensions.cs b/src/CommandLine/CastExtensions.cs new file mode 100644 index 00000000..828a18e4 --- /dev/null +++ b/src/CommandLine/CastExtensions.cs @@ -0,0 +1,100 @@ +using System; +using System.Linq; +using System.Reflection; + +namespace CommandLine +{ + public static class CastExtensions + { + private const string ImplicitCastMethodName = "op_Implicit"; + private const string ExplicitCastMethodName = "op_Explicit"; + + public static bool CanCast(this Type baseType) + { + return baseType.CanImplicitCast() || baseType.CanExplicitCast(); + } + + public static bool CanCast(this object obj) + { + var objType = obj.GetType(); + return objType.CanCast(); + } + + public static T Cast(this object obj) + { + try + { + return (T) obj; + } + catch (InvalidCastException) + { + if (obj.CanImplicitCast()) + return obj.ImplicitCast(); + if (obj.CanExplicitCast()) + return obj.ExplicitCast(); + else + throw; + } + } + + private static bool CanImplicitCast(this Type baseType) + { + return baseType.CanCast(ImplicitCastMethodName); + } + + private static bool CanImplicitCast(this object obj) + { + var baseType = obj.GetType(); + return baseType.CanImplicitCast(); + } + + private static bool CanExplicitCast(this Type baseType) + { + return baseType.CanCast(ExplicitCastMethodName); + } + + private static bool CanExplicitCast(this object obj) + { + var baseType = obj.GetType(); + return baseType.CanExplicitCast(); + } + + private static bool CanCast(this Type baseType, string castMethodName) + { + var targetType = typeof(T); + return baseType.GetMethods(BindingFlags.Public | BindingFlags.Static) + .Where(mi => mi.Name == castMethodName && mi.ReturnType == targetType) + .Any(mi => + { + ParameterInfo pi = mi.GetParameters().FirstOrDefault(); + return pi != null && pi.ParameterType == baseType; + }); + } + + private static T ImplicitCast(this object obj) + { + return obj.Cast(ImplicitCastMethodName); + } + + private static T ExplicitCast(this object obj) + { + return obj.Cast(ExplicitCastMethodName); + } + + private static T Cast(this object obj, string castMethodName) + { + var objType = obj.GetType(); + MethodInfo conversionMethod = objType.GetMethods(BindingFlags.Public | BindingFlags.Static) + .Where(mi => mi.Name == castMethodName && mi.ReturnType == typeof(T)) + .SingleOrDefault(mi => + { + ParameterInfo pi = mi.GetParameters().FirstOrDefault(); + return pi != null && pi.ParameterType == objType; + }); + if (conversionMethod != null) + return (T) conversionMethod.Invoke(null, new[] {obj}); + else + throw new InvalidCastException($"No method to cast {objType.FullName} to {typeof(T).FullName}"); + } + } +} diff --git a/src/CommandLine/Infrastructure/LocalizableAttributeProperty.cs b/src/CommandLine/Infrastructure/LocalizableAttributeProperty.cs index 9edd621b..26714cca 100644 --- a/src/CommandLine/Infrastructure/LocalizableAttributeProperty.cs +++ b/src/CommandLine/Infrastructure/LocalizableAttributeProperty.cs @@ -1,8 +1,5 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Reflection; -using System.Text; namespace CommandLine.Infrastructure { @@ -43,15 +40,16 @@ private string GetLocalizedValue() return _value; if (_localizationPropertyInfo == null) { - // Static class IsAbstract + // Static class IsAbstract if (!_type.IsVisible) throw new ArgumentException($"Invalid resource type '{_type.FullName}'! {_type.Name} is not visible for the parser! Change resources 'Access Modifier' to 'Public'", _propertyName); - PropertyInfo propertyInfo = _type.GetProperty(_value, BindingFlags.Public | BindingFlags.GetProperty | BindingFlags.Static); - if (propertyInfo == null || !propertyInfo.CanRead || propertyInfo.PropertyType != typeof(string)) + PropertyInfo propertyInfo = _type.GetProperty(_value, BindingFlags.Public | BindingFlags.Static); + if (propertyInfo == null || !propertyInfo.CanRead || (propertyInfo.PropertyType != typeof(string) && !propertyInfo.PropertyType.CanCast())) throw new ArgumentException("Invalid resource property name! Localized value: {_value}", _propertyName); _localizationPropertyInfo = propertyInfo; } - return (string)_localizationPropertyInfo.GetValue(null, null); + + return _localizationPropertyInfo.GetValue(null, null).Cast(); } } diff --git a/tests/CommandLine.Tests/Fakes/ResourceFakes.cs b/tests/CommandLine.Tests/Fakes/ResourceFakes.cs index f7b46bac..1b18da6e 100644 --- a/tests/CommandLine.Tests/Fakes/ResourceFakes.cs +++ b/tests/CommandLine.Tests/Fakes/ResourceFakes.cs @@ -3,6 +3,10 @@ public static class StaticResource { public static string HelpText { get { return "Localized HelpText"; } } + public static TypeWithImplicitCast ImplicitCastHelpText => new TypeWithImplicitCast("Localized HelpText"); + public static TypeWithExplicitCast ExplicitCastHelpText => new TypeWithExplicitCast("Localized HelpText"); + public static TypeWithWrongImplicitCast WrongImplicitCastHelpText => new TypeWithWrongImplicitCast(); + public static TypeWithWrongExplicitCast WrongExplicitCastHelpText => new TypeWithWrongExplicitCast(); } public class NonStaticResource @@ -10,6 +14,10 @@ public class NonStaticResource public static string HelpText { get { return "Localized HelpText"; } } public static string WriteOnlyText { set { value?.ToString(); } } private static string PrivateHelpText { get { return "Localized HelpText"; } } + public static TypeWithImplicitCast ImplicitCastHelpText => new TypeWithImplicitCast("Localized HelpText"); + public static TypeWithExplicitCast ExplicitCastHelpText => new TypeWithExplicitCast("Localized HelpText"); + public static TypeWithWrongImplicitCast WrongImplicitCastHelpText => new TypeWithWrongImplicitCast(); + public static TypeWithWrongExplicitCast WrongExplicitCastHelpText => new TypeWithWrongExplicitCast(); } public class NonStaticResource_WithNonStaticProperty @@ -22,4 +30,59 @@ internal class InternalResource public static string HelpText { get { return "Localized HelpText"; } } } + public class TypeWithImplicitCast + { + private string value; + + public TypeWithImplicitCast(string value) + { + this.value = value; + } + + public static implicit operator string(TypeWithImplicitCast obj) + { + return obj.value; + } + + public static implicit operator int(TypeWithImplicitCast obj) + { + return 0; + } + } + + public class TypeWithWrongImplicitCast + { + public static implicit operator int(TypeWithWrongImplicitCast obj) + { + return 0; + } + } + + public class TypeWithExplicitCast + { + private string value; + + public TypeWithExplicitCast(string value) + { + this.value = value; + } + + public static explicit operator string(TypeWithExplicitCast obj) + { + return obj.value; + } + + public static explicit operator int(TypeWithExplicitCast obj) + { + return 0; + } + } + + public class TypeWithWrongExplicitCast + { + public static explicit operator int(TypeWithWrongExplicitCast obj) + { + return 0; + } + } } diff --git a/tests/CommandLine.Tests/Unit/BaseAttributeTests.cs b/tests/CommandLine.Tests/Unit/BaseAttributeTests.cs index 3c2bfbbd..ab566255 100644 --- a/tests/CommandLine.Tests/Unit/BaseAttributeTests.cs +++ b/tests/CommandLine.Tests/Unit/BaseAttributeTests.cs @@ -21,12 +21,16 @@ public static void Default(object defaultValue) [InlineData("Help text", null, "Help text")] [InlineData("HelpText", typeof(Fakes.StaticResource), "Localized HelpText")] [InlineData("HelpText", typeof(Fakes.NonStaticResource), "Localized HelpText")] + [InlineData("ImplicitCastHelpText", typeof(Fakes.StaticResource), "Localized HelpText")] + [InlineData("ImplicitCastHelpText", typeof(Fakes.NonStaticResource), "Localized HelpText")] + [InlineData("ExplicitCastHelpText", typeof(Fakes.StaticResource), "Localized HelpText")] + [InlineData("ExplicitCastHelpText", typeof(Fakes.NonStaticResource), "Localized HelpText")] public static void HelpText(string helpText, Type resourceType, string expected) { TestBaseAttribute baseAttribute = new TestBaseAttribute(); baseAttribute.HelpText = helpText; baseAttribute.ResourceType = resourceType; - + Assert.Equal(expected, baseAttribute.HelpText); } @@ -35,6 +39,10 @@ public static void HelpText(string helpText, Type resourceType, string expected) [InlineData("WriteOnlyText", typeof(Fakes.NonStaticResource))] [InlineData("PrivateOnlyText", typeof(Fakes.NonStaticResource))] [InlineData("HelpText", typeof(Fakes.InternalResource))] + [InlineData("WrongImplicitCastHelpText", typeof(Fakes.StaticResource))] + [InlineData("WrongExplicitCastHelpText", typeof(Fakes.StaticResource))] + [InlineData("WrongImplicitCastHelpText", typeof(Fakes.NonStaticResource))] + [InlineData("WrongExplicitCastHelpText", typeof(Fakes.NonStaticResource))] public void ThrowsHelpText(string helpText, Type resourceType) { TestBaseAttribute baseAttribute = new TestBaseAttribute();