This commit is contained in:
Tyrrrz
2024-06-16 01:31:34 +03:00
parent 2323a57c39
commit 3fc7054f80
21 changed files with 195 additions and 148 deletions

View File

@@ -6,7 +6,7 @@ namespace CliFx.Attributes;
/// Annotates a type that defines a command. /// Annotates a type that defines a command.
/// </summary> /// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false)] [AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class CommandAttribute : Attribute public class CommandAttribute : Attribute
{ {
/// <summary> /// <summary>
/// Initializes an instance of <see cref="CommandAttribute" />. /// Initializes an instance of <see cref="CommandAttribute" />.

View File

@@ -0,0 +1,16 @@
namespace CliFx.Attributes;
/// <summary>
/// Annotates a property that defines the help option for a command.
/// </summary>
public class CommandHelpOptionAttribute : CommandOptionAttribute
{
/// <summary>
/// Initializes an instance of <see cref="CommandHelpOptionAttribute" />.
/// </summary>
public CommandHelpOptionAttribute()
: base("help", 'h')
{
Description = "Show help for this command.";
}
}

View File

@@ -7,7 +7,7 @@ namespace CliFx.Attributes;
/// Annotates a property that defines a command option. /// Annotates a property that defines a command option.
/// </summary> /// </summary>
[AttributeUsage(AttributeTargets.Property)] [AttributeUsage(AttributeTargets.Property)]
public sealed class CommandOptionAttribute : Attribute public class CommandOptionAttribute : Attribute
{ {
/// <summary> /// <summary>
/// Initializes an instance of <see cref="CommandOptionAttribute" />. /// Initializes an instance of <see cref="CommandOptionAttribute" />.

View File

@@ -7,7 +7,7 @@ namespace CliFx.Attributes;
/// Annotates a property that defines a command parameter. /// Annotates a property that defines a command parameter.
/// </summary> /// </summary>
[AttributeUsage(AttributeTargets.Property)] [AttributeUsage(AttributeTargets.Property)]
public sealed class CommandParameterAttribute(int order) : Attribute public class CommandParameterAttribute(int order) : Attribute
{ {
/// <summary> /// <summary>
/// Parameter order. /// Parameter order.

View File

@@ -0,0 +1,16 @@
namespace CliFx.Attributes;
/// <summary>
/// Annotates a property that defines the version option for a command.
/// </summary>
public class CommandVersionOptionAttribute : CommandOptionAttribute
{
/// <summary>
/// Initializes an instance of <see cref="CommandVersionOptionAttribute" />.
/// </summary>
public CommandVersionOptionAttribute()
: base("version")
{
Description = "Show application version.";
}
}

View File

@@ -41,16 +41,6 @@ public class CliApplication(
private bool IsPreviewModeEnabled(CommandInput commandInput) => private bool IsPreviewModeEnabled(CommandInput commandInput) =>
Configuration.IsPreviewModeAllowed && commandInput.IsPreviewDirectiveSpecified; Configuration.IsPreviewModeAllowed && commandInput.IsPreviewDirectiveSpecified;
private bool ShouldShowHelpText(CommandSchema commandSchema, CommandInput commandInput) =>
commandSchema.IsHelpOptionAvailable && commandInput.IsHelpOptionSpecified
||
// Show help text also if the fallback default command is executed without any arguments
commandSchema == FallbackDefaultCommand.Schema
&& !commandInput.HasArguments;
private bool ShouldShowVersionText(CommandSchema commandSchema, CommandInput commandInput) =>
commandSchema.IsVersionOptionAvailable && commandInput.IsVersionOptionSpecified;
private async ValueTask PromptDebuggerAsync() private async ValueTask PromptDebuggerAsync()
{ {
using (console.WithForegroundColor(ConsoleColor.Green)) using (console.WithForegroundColor(ConsoleColor.Green))
@@ -119,30 +109,36 @@ public class CliApplication(
commandSchema.GetValues(commandInstance) commandSchema.GetValues(commandInstance)
); );
// Handle the help option
if (ShouldShowHelpText(commandSchema, commandInput))
{
console.WriteHelpText(helpContext);
return 0;
}
// Handle the version option
if (ShouldShowVersionText(commandSchema, commandInput))
{
console.WriteLine(Metadata.Version);
return 0;
}
// Starting from this point, we may produce exceptions that are meant for the // Starting from this point, we may produce exceptions that are meant for the
// end-user of the application (i.e. invalid input, command exception, etc). // end-user of the application (i.e. invalid input, command exception, etc).
// Catch these exceptions here, print them to the console, and don't let them // Catch these exceptions here, print them to the console, and don't let them
// propagate further. // propagate further.
try try
{ {
// Bind and execute the command // Bind the command input to the command instance
_commandBinder.Bind(commandInput, commandSchema, commandInstance); _commandBinder.Bind(commandInput, commandSchema, commandInstance);
await commandInstance.ExecuteAsync(console);
// Handle the version option
if (commandInstance is ICommandWithVersionOption { IsVersionRequested: true })
{
console.WriteLine(Metadata.Version);
return 0;
}
// Handle the help option
if (
commandInstance
is ICommandWithHelpOption { IsHelpRequested: true }
// Fallback default command always shows help, even if the option is not specified
or FallbackDefaultCommand
)
{
console.WriteHelpText(helpContext);
return 0;
}
// Execute the command
await commandInstance.ExecuteAsync(console);
return 0; return 0;
} }
catch (CliFxException ex) catch (CliFxException ex)

View File

@@ -175,7 +175,7 @@ public partial class CliApplicationBuilder
[UnconditionalSuppressMessage( [UnconditionalSuppressMessage(
"SingleFile", "SingleFile",
"IL3000:Avoid accessing Assembly file path when publishing as a single file", "IL3000:Avoid accessing Assembly file path when publishing as a single file",
Justification = "The return value of the method is checked to ensure the assembly location is available." Justification = "The file path is checked to ensure the assembly location is available."
)] )]
private static string GetDefaultExecutableName() private static string GetDefaultExecutableName()
{ {

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0</TargetFrameworks> <TargetFrameworks>netstandard2.0;netstandard2.1;net7.0;net8.0</TargetFrameworks>
<IsPackable>true</IsPackable> <IsPackable>true</IsPackable>
<IsTrimmable Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net6.0'))">true</IsTrimmable> <IsTrimmable Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net6.0'))">true</IsTrimmable>
<IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">true</IsAotCompatible> <IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">true</IsAotCompatible>

View File

@@ -16,7 +16,7 @@ internal class CommandBinder(ITypeActivator typeActivator)
{ {
private readonly IFormatProvider _formatProvider = CultureInfo.InvariantCulture; private readonly IFormatProvider _formatProvider = CultureInfo.InvariantCulture;
private object? ConvertSingle(IInputSchema inputSchema, string? rawValue, Type targetType) private object? ConvertSingle(InputSchema inputSchema, string? rawValue, Type targetType)
{ {
// Custom converter // Custom converter
if (inputSchema.Converter is not null) if (inputSchema.Converter is not null)
@@ -103,7 +103,7 @@ internal class CommandBinder(ITypeActivator typeActivator)
} }
private object? ConvertMultiple( private object? ConvertMultiple(
IInputSchema inputSchema, InputSchema inputSchema,
IReadOnlyList<string> rawValues, IReadOnlyList<string> rawValues,
Type targetEnumerableType, Type targetEnumerableType,
Type targetElementType Type targetElementType
@@ -137,7 +137,7 @@ internal class CommandBinder(ITypeActivator typeActivator)
); );
} }
private object? ConvertMember(IInputSchema inputSchema, IReadOnlyList<string> rawValues) private object? ConvertMember(InputSchema inputSchema, IReadOnlyList<string> rawValues)
{ {
try try
{ {
@@ -192,7 +192,7 @@ internal class CommandBinder(ITypeActivator typeActivator)
); );
} }
private void ValidateMember(IInputSchema inputSchema, object? convertedValue) private void ValidateMember(InputSchema inputSchema, object? convertedValue)
{ {
var errors = new List<BindingValidationError>(); var errors = new List<BindingValidationError>();
@@ -218,7 +218,7 @@ internal class CommandBinder(ITypeActivator typeActivator)
} }
private void BindMember( private void BindMember(
IInputSchema inputSchema, InputSchema inputSchema,
ICommand commandInstance, ICommand commandInstance,
IReadOnlyList<string> rawValues IReadOnlyList<string> rawValues
) )
@@ -335,7 +335,7 @@ internal class CommandBinder(ITypeActivator typeActivator)
// Environment variable // Environment variable
else if (environmentVariableInput is not null) else if (environmentVariableInput is not null)
{ {
var rawValues = optionSchema.IsScalar var rawValues = optionSchema.IsSequence
? [environmentVariableInput.Value] ? [environmentVariableInput.Value]
: environmentVariableInput.SplitValues(); : environmentVariableInput.SplitValues();

View File

@@ -9,8 +9,14 @@ namespace CliFx;
// Fallback command used when the application doesn't have one configured. // Fallback command used when the application doesn't have one configured.
// This command is only used as a stub for help text. // This command is only used as a stub for help text.
[Command] [Command]
internal partial class FallbackDefaultCommand : ICommand internal partial class FallbackDefaultCommand : ICommandWithHelpOption, ICommandWithVersionOption
{ {
[CommandHelpOption]
public bool IsHelpRequested { get; init; }
[CommandVersionOption]
public bool IsVersionRequested { get; init; }
// Never actually executed // Never actually executed
[ExcludeFromCodeCoverage] [ExcludeFromCodeCoverage]
public ValueTask ExecuteAsync(IConsole console) => default; public ValueTask ExecuteAsync(IConsole console) => default;

View File

@@ -69,7 +69,7 @@ internal class HelpConsoleFormatter(ConsoleWriter consoleWriter, HelpContext con
{ {
Write( Write(
ConsoleColor.DarkCyan, ConsoleColor.DarkCyan,
parameter.Property.IsScalar() ? $"<{parameter.Name}>" : $"<{parameter.Name}...>" parameter.IsSequence ? $"<{parameter.Name}...>" : $"<{parameter.Name}>"
); );
Write(' '); Write(' ');
} }
@@ -85,7 +85,7 @@ internal class HelpConsoleFormatter(ConsoleWriter consoleWriter, HelpContext con
); );
Write(' '); Write(' ');
Write(ConsoleColor.White, option.Property.IsScalar() ? "<value>" : "<values...>"); Write(ConsoleColor.White, option.IsSequence ? "<values...>" : "<value>");
Write(' '); Write(' ');
} }
@@ -170,8 +170,8 @@ internal class HelpConsoleFormatter(ConsoleWriter consoleWriter, HelpContext con
} }
// Valid values // Valid values
var validValues = parameterSchema.Property.GetValidValues(); var validValues = parameterSchema.Property.TryGetValidValues();
if (validValues.Any()) if (validValues?.Any() == true)
{ {
Write(ConsoleColor.White, "Choices: "); Write(ConsoleColor.White, "Choices: ");
@@ -257,8 +257,8 @@ internal class HelpConsoleFormatter(ConsoleWriter consoleWriter, HelpContext con
} }
// Valid values // Valid values
var validValues = optionSchema.Property.GetValidValues(); var validValues = optionSchema.Property.TryGetValidValues();
if (validValues.Any()) if (validValues?.Any() == true)
{ {
Write(ConsoleColor.White, "Choices: "); Write(ConsoleColor.White, "Choices: ");
@@ -305,7 +305,7 @@ internal class HelpConsoleFormatter(ConsoleWriter consoleWriter, HelpContext con
} }
} }
private void WriteDefaultValue(IMemberSchema schema) private void WriteDefaultValue(InputSchema schema)
{ {
var defaultValue = context.CommandDefaultValues.GetValueOrDefault(schema); var defaultValue = context.CommandDefaultValues.GetValueOrDefault(schema);
if (defaultValue is not null) if (defaultValue is not null)

View File

@@ -7,7 +7,7 @@ internal class HelpContext(
ApplicationMetadata applicationMetadata, ApplicationMetadata applicationMetadata,
ApplicationSchema applicationSchema, ApplicationSchema applicationSchema,
CommandSchema commandSchema, CommandSchema commandSchema,
IReadOnlyDictionary<IInputSchema, object?> commandDefaultValues IReadOnlyDictionary<InputSchema, object?> commandDefaultValues
) )
{ {
public ApplicationMetadata ApplicationMetadata { get; } = applicationMetadata; public ApplicationMetadata ApplicationMetadata { get; } = applicationMetadata;
@@ -16,6 +16,6 @@ internal class HelpContext(
public CommandSchema CommandSchema { get; } = commandSchema; public CommandSchema CommandSchema { get; } = commandSchema;
public IReadOnlyDictionary<IInputSchema, object?> CommandDefaultValues { get; } = public IReadOnlyDictionary<InputSchema, object?> CommandDefaultValues { get; } =
commandDefaultValues; commandDefaultValues;
} }

View File

@@ -0,0 +1,12 @@
namespace CliFx;
/// <summary>
/// Command definition that includes the help option.
/// </summary>
public interface ICommandWithHelpOption : ICommand
{
/// <summary>
/// Whether the user requested help for this command (via the `-h|--help` option).
/// </summary>
bool IsHelpRequested { get; }
}

View File

@@ -0,0 +1,12 @@
namespace CliFx;
/// <summary>
/// Command definition that includes the version option.
/// </summary>
public interface ICommandWithVersionOption : ICommand
{
/// <summary>
/// Whether the user requested the version information (via the `--version` option).
/// </summary>
bool IsVersionRequested { get; }
}

View File

@@ -16,7 +16,7 @@ public class CommandSchema(
) )
{ {
/// <summary> /// <summary>
/// Command's CLR type. /// Underlying CLR type of the command.
/// </summary> /// </summary>
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
public Type Type { get; } = type; public Type Type { get; } = type;
@@ -26,6 +26,11 @@ public class CommandSchema(
/// </summary> /// </summary>
public string? Name { get; } = name; public string? Name { get; } = name;
/// <summary>
/// Whether this command is the application's default command.
/// </summary>
public bool IsDefault { get; } = string.IsNullOrWhiteSpace(name);
/// <summary> /// <summary>
/// Command description. /// Command description.
/// </summary> /// </summary>
@@ -41,19 +46,14 @@ public class CommandSchema(
/// </summary> /// </summary>
public IReadOnlyList<OptionSchema> Options { get; } = options; public IReadOnlyList<OptionSchema> Options { get; } = options;
/// <summary>
/// Whether this command is the application's default command.
/// </summary>
public bool IsDefault { get; } = string.IsNullOrWhiteSpace(name);
internal bool MatchesName(string? name) => internal bool MatchesName(string? name) =>
!string.IsNullOrWhiteSpace(Name) !string.IsNullOrWhiteSpace(Name)
? string.Equals(name, Name, StringComparison.OrdinalIgnoreCase) ? string.Equals(name, Name, StringComparison.OrdinalIgnoreCase)
: string.IsNullOrWhiteSpace(name); : string.IsNullOrWhiteSpace(name);
internal IReadOnlyDictionary<IInputSchema, object?> GetValues(ICommand instance) internal IReadOnlyDictionary<InputSchema, object?> GetValues(ICommand instance)
{ {
var result = new Dictionary<IInputSchema, object?>(); var result = new Dictionary<InputSchema, object?>();
foreach (var parameterSchema in Parameters) foreach (var parameterSchema in Parameters)
{ {

View File

@@ -1,25 +0,0 @@
using System.Collections.Generic;
using CliFx.Extensibility;
namespace CliFx.Schema;
/// <summary>
/// Describes an input of a command, which can be either a parameter or an option.
/// </summary>
public interface IInputSchema
{
/// <summary>
/// Describes the binding of this input to a CLR property.
/// </summary>
PropertyBinding Property { get; }
/// <summary>
/// Optional binding converter for this input.
/// </summary>
IBindingConverter? Converter { get; }
/// <summary>
/// Optional binding validator(s) for this input.
/// </summary>
IReadOnlyList<IBindingValidator> Validators { get; }
}

View File

@@ -0,0 +1,35 @@
using System.Collections.Generic;
using CliFx.Extensibility;
namespace CliFx.Schema;
/// <summary>
/// Describes an input of a command, which can be either a parameter or an option.
/// </summary>
public abstract class InputSchema(
PropertyBinding property,
bool isSequence,
IBindingConverter? converter,
IReadOnlyList<IBindingValidator> validators
)
{
/// <summary>
/// CLR property to which this input is bound.
/// </summary>
public PropertyBinding Property { get; } = property;
/// <summary>
/// Whether this input can accept more than one value.
/// </summary>
public bool IsSequence { get; } = isSequence;
/// <summary>
/// Optional binding converter for this input.
/// </summary>
public IBindingConverter? Converter { get; } = converter;
/// <summary>
/// Optional binding validator(s) for this input.
/// </summary>
public IReadOnlyList<IBindingValidator> Validators { get; } = validators;
}

View File

@@ -6,12 +6,11 @@ using CliFx.Extensibility;
namespace CliFx.Schema; namespace CliFx.Schema;
/// <summary> /// <summary>
/// Describes a command's option. /// Describes an option input of a command.
/// </summary> /// </summary>
public class OptionSchema( public class OptionSchema(
PropertyBinding property, PropertyBinding property,
bool isScalar, bool isSequence,
IReadOnlyList<object?>? validValues,
string? name, string? name,
char? shortName, char? shortName,
string? environmentVariable, string? environmentVariable,
@@ -19,17 +18,8 @@ public class OptionSchema(
string? description, string? description,
IBindingConverter? converter, IBindingConverter? converter,
IReadOnlyList<IBindingValidator> validators IReadOnlyList<IBindingValidator> validators
) : IInputSchema ) : InputSchema(property, isSequence, converter, validators)
{ {
/// <inheritdoc />
public PropertyBinding Property { get; } = property;
/// <inheritdoc />
public bool IsScalar { get; } = isScalar;
/// <inheritdoc />
public IReadOnlyList<object?>? ValidValues { get; } = validValues;
/// <summary> /// <summary>
/// Option name. /// Option name.
/// </summary> /// </summary>
@@ -55,12 +45,6 @@ public class OptionSchema(
/// </summary> /// </summary>
public string? Description { get; } = description; public string? Description { get; } = description;
/// <inheritdoc />
public IBindingConverter? Converter { get; } = converter;
/// <inheritdoc />
public IReadOnlyList<IBindingValidator> Validators { get; } = validators;
internal bool MatchesName(string? name) => internal bool MatchesName(string? name) =>
!string.IsNullOrWhiteSpace(Name) !string.IsNullOrWhiteSpace(Name)
&& string.Equals(Name, name, StringComparison.OrdinalIgnoreCase); && string.Equals(Name, name, StringComparison.OrdinalIgnoreCase);

View File

@@ -4,29 +4,19 @@ using CliFx.Extensibility;
namespace CliFx.Schema; namespace CliFx.Schema;
/// <summary> /// <summary>
/// Describes a command's parameter. /// Describes a parameter input of a command.
/// </summary> /// </summary>
public class ParameterSchema( public class ParameterSchema(
PropertyBinding property, PropertyBinding property,
bool isScalar, bool isSequence,
IReadOnlyList<object?>? validValues,
int order, int order,
string name, string name,
bool isRequired, bool isRequired,
string? description, string? description,
IBindingConverter? converter, IBindingConverter? converter,
IReadOnlyList<IBindingValidator> validators IReadOnlyList<IBindingValidator> validators
) : IInputSchema ) : InputSchema(property, isSequence, converter, validators)
{ {
/// <inheritdoc />
public PropertyBinding Property { get; } = property;
/// <inheritdoc />
public bool IsScalar { get; } = isScalar;
/// <inheritdoc />
public IReadOnlyList<object?>? ValidValues { get; } = validValues;
/// <summary> /// <summary>
/// Order, in which the parameter is bound from the command-line arguments. /// Order, in which the parameter is bound from the command-line arguments.
/// </summary> /// </summary>
@@ -47,12 +37,5 @@ public class ParameterSchema(
/// </summary> /// </summary>
public string? Description { get; } = description; public string? Description { get; } = description;
/// <inheritdoc /> internal string GetFormattedIdentifier() => IsSequence ? $"<{Name}>" : $"<{Name}...>";
public IBindingConverter? Converter { get; } = converter;
/// <inheritdoc />
public IReadOnlyList<IBindingValidator> Validators { get; } = validators;
internal string GetFormattedIdentifier() =>
IsScalar ? $"<{Name}>" : $"<{Name}...>";
} }

View File

@@ -5,18 +5,18 @@ using System.Linq;
namespace CliFx.Schema; namespace CliFx.Schema;
/// <summary> /// <summary>
/// Describes a CLR property. /// Represents a binding to a CLR property.
/// </summary> /// </summary>
public class PropertyBinding( public class PropertyBinding(
Type propertyType, Type type,
Func<object, object?> getValue, Func<object, object?> getValue,
Action<object, object?> setValue Action<object, object?> setValue
) )
{ {
/// <summary> /// <summary>
/// Underlying property type. /// Underlying CLR type of the property.
/// </summary> /// </summary>
public Type PropertyType { get; } = propertyType; public Type Type { get; } = type;
/// <summary> /// <summary>
/// Gets the current value of the property on the specified instance. /// Gets the current value of the property on the specified instance.
@@ -27,12 +27,23 @@ public class PropertyBinding(
/// Sets the value of the property on the specified instance. /// Sets the value of the property on the specified instance.
/// </summary> /// </summary>
public void SetValue(object instance, object? value) => setValue(instance, value); public void SetValue(object instance, object? value) => setValue(instance, value);
}
internal static class PropertyBindingExtensions internal IReadOnlyList<object?>? TryGetValidValues()
{ {
public static IReadOnlyList<object?>? TryGetValidValues(this PropertyBinding binding) => if (Type.IsEnum)
binding.PropertyType.IsEnum {
? binding.PropertyType.GetEnumValuesAsUnderlyingType().Cast<object?>().ToArray() var values =
: null; #if NET7_0_OR_GREATER
Type.GetEnumValuesAsUnderlyingType();
#else
// AOT-compatible APIs are not available here, but it's unlikely
// someone will be AOT-compiling a net6.0 or older app anyway.
Type.GetEnumValues();
#endif
return values.Cast<object?>().ToArray();
}
return null;
}
} }

View File

@@ -1,6 +1,7 @@
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
@@ -8,13 +9,17 @@ namespace CliFx.Utils.Extensions;
internal static class TypeExtensions internal static class TypeExtensions
{ {
public static bool Implements(this Type type, Type interfaceType) => public static bool Implements(
type.GetInterfaces().Contains(interfaceType); [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] this Type type,
Type interfaceType
) => type.GetInterfaces().Contains(interfaceType);
public static Type? TryGetNullableUnderlyingType(this Type type) => public static Type? TryGetNullableUnderlyingType(this Type type) =>
Nullable.GetUnderlyingType(type); Nullable.GetUnderlyingType(type);
public static Type? TryGetEnumerableUnderlyingType(this Type type) public static Type? TryGetEnumerableUnderlyingType(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] this Type type
)
{ {
if (type.IsPrimitive) if (type.IsPrimitive)
return null; return null;
@@ -35,24 +40,20 @@ internal static class TypeExtensions
} }
public static MethodInfo? TryGetStaticParseMethod( public static MethodInfo? TryGetStaticParseMethod(
this Type type, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] this Type type,
bool withFormatProvider = false bool withFormatProvider = false
) ) =>
{ type.GetMethod(
var argumentTypes = withFormatProvider
? new[] { typeof(string), typeof(IFormatProvider) }
: new[] { typeof(string) };
return type.GetMethod(
"Parse", "Parse",
BindingFlags.Public | BindingFlags.Static, BindingFlags.Public | BindingFlags.Static,
null, null,
argumentTypes, withFormatProvider ? [typeof(string), typeof(IFormatProvider)] : [typeof(string)],
null null
); );
}
public static bool IsToStringOverriden(this Type type) public static bool IsToStringOverriden(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] this Type type
)
{ {
var toStringMethod = type.GetMethod(nameof(ToString), Type.EmptyTypes); var toStringMethod = type.GetMethod(nameof(ToString), Type.EmptyTypes);
return toStringMethod?.GetBaseDefinition()?.DeclaringType != toStringMethod?.DeclaringType; return toStringMethod?.GetBaseDefinition()?.DeclaringType != toStringMethod?.DeclaringType;