Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Throw invalid operation exception if Dictionary type is used for specification #889

Open
wants to merge 18 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''

---

**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
Either fork from this fiddle and paste link here: https://dotnetfiddle.net/mh9CjX

or

Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error

**Expected behavior**
A clear and concise description of what you expected to happen.

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Additional context**
Add any other context about the problem here.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file.

CommandLineParser project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.9.0-preview2]

### Added
- Properly assign arguments after a double dash to values, fix #605 by [@robnasby, PR# 610](https://github.com/commandlineparser/commandline/pull/610).

### Changed
- Drop "Add multi-instance option support".


## [2.9.0-preview1] - 2020-7-24

### Added
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ __This library provides _hassle free_ command line parsing with a constantly upd
- Support Mutable and Immutable types.
- Support HelpText localization.
- Support ordering of options in HelpText.
- Support [Mutually Exclusive Options](https://github.com/commandlineparser/commandline/wiki/Mutually-Exclusive-Options) and Options groups.
- Support [Mutually Exclusive Options](https://github.com/commandlineparser/commandline/wiki/Mutually-Exclusive-Options) and [Option groups](https://github.com/commandlineparser/commandline/wiki/Option-Groups).
- Support named and value options.
- Support Asynchronous programming with async and await.
- Unparsing support: `CommandLine.Parser.Default.FormatCommandLine<T>(T options)`.
Expand Down
13 changes: 2 additions & 11 deletions appveyor.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#version should be only changed with RELEASE eminent, see RELEASE.md

version: 2.9.0-ci-{build}
version: 2.9.1-ci-{build}

image: Visual Studio 2019

Expand Down Expand Up @@ -44,18 +44,9 @@ on_failure:
appveyor PushArtifact .\files.lst -DeploymentName "Failed Build File Listing"

deploy:
- provider: GitHub
auth_token:
secure: hVyVwHl0JiVq0VxXB4VMRWbUtrGclIzadfnWFcWCQBLvbgMLahLBnWlwGglT63pZ
artifact: /.*(\.|\.s)nupkg/
prerelease: false
force_update: true #fsharp package runs as separate build job, so have to force_update to add fsharp.nuget added
on:
APPVEYOR_REPO_TAG: true

- provider: NuGet
api_key:
secure: e2gJJ3r6Uls5trJwryaudAZd49QniNfIjax/A+tfywlchSnIQVOzOQCO9tTSNccI
secure: llMIgYMuLHh9thyKMEAmkWraTaA9Zvcm1F8/yRwm0HCiPIt/ehR/GI4kJKyMTPyf
artifact: /.*(\.|\.s)nupkg/
on:
APPVEYOR_REPO_TAG: true

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion demo/ReadText.LocalizedDemo/Properties/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@
<value>A sequence option '{0}' is defined with fewer or more items than required.</value>
</data>
<data name="SentenceSequenceOutOfRangeErrorValue" xml:space="preserve">
<value>A sequence value not bound to option name is defined with few items than required.</value>
<value>A sequence value not bound to option name is defined with fewer items than required.</value>
</data>
<data name="SentenceSetValueExceptionError" xml:space="preserve">
<value>Error setting value to option '{0}': {1}</value>
Expand Down
100 changes: 100 additions & 0 deletions src/CommandLine/CastExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using System;
using System.Linq;
using System.Reflection;

namespace CommandLine
{
internal static class CastExtensions
{
private const string ImplicitCastMethodName = "op_Implicit";
private const string ExplicitCastMethodName = "op_Explicit";

public static bool CanCast<T>(this Type baseType)
{
return baseType.CanImplicitCast<T>() || baseType.CanExplicitCast<T>();
}

public static bool CanCast<T>(this object obj)
{
var objType = obj.GetType();
return objType.CanCast<T>();
}

public static T Cast<T>(this object obj)
{
try
{
return (T) obj;
}
catch (InvalidCastException)
{
if (obj.CanImplicitCast<T>())
return obj.ImplicitCast<T>();
if (obj.CanExplicitCast<T>())
return obj.ExplicitCast<T>();
else
throw;
}
}

private static bool CanImplicitCast<T>(this Type baseType)
{
return baseType.CanCast<T>(ImplicitCastMethodName);
}

private static bool CanImplicitCast<T>(this object obj)
{
var baseType = obj.GetType();
return baseType.CanImplicitCast<T>();
}

private static bool CanExplicitCast<T>(this Type baseType)
{
return baseType.CanCast<T>(ExplicitCastMethodName);
}

private static bool CanExplicitCast<T>(this object obj)
{
var baseType = obj.GetType();
return baseType.CanExplicitCast<T>();
}

private static bool CanCast<T>(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<T>(this object obj)
{
return obj.Cast<T>(ImplicitCastMethodName);
}

private static T ExplicitCast<T>(this object obj)
{
return obj.Cast<T>(ExplicitCastMethodName);
}

private static T Cast<T>(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}");
}
}
}
8 changes: 7 additions & 1 deletion src/CommandLine/Core/SpecificationGuards.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ static class SpecificationGuards
Tuple.Create(GuardAgainstScalarWithRange(), "Scalar option specifications do not support range specification."),
Tuple.Create(GuardAgainstSequenceWithWrongRange(), "Bad range in sequence option specifications."),
Tuple.Create(GuardAgainstSequenceWithZeroRange(), "Zero is not allowed in range of sequence option specifications."),
Tuple.Create(GuardAgainstOneCharLongName(), "Long name should be longer than one character.")
Tuple.Create(GuardAgainstOneCharLongName(), "Long name should be longer than one character."),
Tuple.Create(GaurdAgainstUnsupportedSequenceTypes(), "Unsupported sequence type specification.")
};

private static Func<Specification, bool> GuardAgainstScalarWithRange()
Expand All @@ -39,5 +40,10 @@ private static Func<Specification, bool> GuardAgainstSequenceWithZeroRange()
&& (spec.HavingMin(min => min == 0)
|| spec.HavingMax(max => max == 0));
}

private static Func<Specification, bool> GaurdAgainstUnsupportedSequenceTypes()
{
return spec => spec.TargetType == TargetType.Sequence && spec.ConversionType.GetGenericArguments().Length != 1;
}
}
}
10 changes: 4 additions & 6 deletions src/CommandLine/Infrastructure/LocalizableAttributeProperty.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;

namespace CommandLine.Infrastructure
{
Expand Down Expand Up @@ -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))
if (propertyInfo == null || !propertyInfo.CanRead || (propertyInfo.PropertyType != typeof(string) && !propertyInfo.PropertyType.CanCast<string>()))
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<string>();
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/CommandLine/Text/SentenceBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ public override Func<Error, string> FormatError
case ErrorType.SequenceOutOfRangeError:
var seqOutRange = ((SequenceOutOfRangeError)error);
return seqOutRange.NameInfo.Equals(NameInfo.EmptyName)
? "A sequence value not bound to option name is defined with few items than required."
? "A sequence value not bound to option name is defined with fewer items than required."
: "A sequence option '".JoinTo(seqOutRange.NameInfo.NameText,
"' is defined with fewer or more items than required.");
case ErrorType.BadVerbSelectedError:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CommandLine.Tests.Fakes
{
class Options_With_Dictionary_Specification
{
[Option('d', "dict")]
public Dictionary<string, string> KeyValuePairs { get; set; }
}
}
63 changes: 63 additions & 0 deletions tests/CommandLine.Tests/Fakes/ResourceFakes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,21 @@
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
{
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
Expand All @@ -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;
}
}
}
10 changes: 9 additions & 1 deletion tests/CommandLine.Tests/Unit/BaseAttributeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand All @@ -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();
Expand Down
Loading