From adaac276b11c30c494a2b9ec87b780f701d4ac9d Mon Sep 17 00:00:00 2001 From: Phil Scott Date: Mon, 25 Nov 2024 22:22:18 -0500 Subject: [PATCH] Refactor CLI Project for AOT Compatibility and Trimming Support Summary: This commit refactors the Spectre.Console.Cli project to replace reflection-based object creation with explicit and type-safe constructs to enable support for Ahead-Of-Time (AOT) compilation and code trimming. AOT mode is only enabled for .NET 9. Introduce DynamicallyAccessedMembers Annotations: * Annotated various types and generic parameters (e.g., CommandApp, CommandSettings) with [DynamicallyAccessedMembers] to ensure necessary metadata is preserved during trimming for AOT scenarios. Explicit Object Creation: * Introduced CreateInstanceHelpers to encapsulate type-safe and explicit instance creation logic. This replaces Activator.CreateInstance for array, multimap, and command settings instantiation. Refactor Command Attributes: * Updated constructors of CommandArgumentAttribute and CommandOptionAttribute to explicitly accept Type for argument and option types, with corresponding logic to register instance builders for these types. Remove Reflection-Heavy Logic: * Replaced uses of Activator.CreateInstance across multiple files with explicit methods from CreateInstanceHelpers. * Removed reflection-reliant dynamic code annotations ([RequiresDynamicCode]) from critical paths where alternatives are now implemented. Enhanced Type Conversion: * Consolidated type conversion logic into TypeConverterHelper with static converters for intrinsic and complex types. * Ensured compatibility by defaulting to explicit conversion mechanisms where possible. Array and Collection Initialization: * Migrated array and collection initialization to CreateInstanceHelpers to handle both primitive and complex types without runtime reflection. Builder and Registrar Adjustments: * Refactored ITypeRegistrar and related classes to include explicit type member annotations, ensuring required metadata survives trimming. * Adjusted configurators (Configurator, IConfigurator) to support type-safe default and added commands. Testing Adjustments: * Added internal visibility to support AOT testing scenarios. * Introduced new project files for trimming-specific tests. Risks ========================= Unit Testing: I tried my best to figure out if I could get the TrimTest console project to run the Cli unit tests when published to AOT via xunit.runner.console, but I'm just not smart enough. I'd feel infinitely better about things if it could do so. As is, the features included there do all work. This project is needed to light up the analyzers properly. Explicit Type Requirement: Attributes now require explicit Type definitions for non-intrinsic types (e.g., DirectoryInfo). Users must specify the argumentType or optionType parameter for such types. Failure to do so will result in runtime exceptions or incorrect behavior. Compatibility with Existing Registrations: Type Registrar Changes: Registrations now require explicit [DynamicallyAccessedMembers] annotations on implementation types. Users with their own custom registration and resolver will need to adapt to match Array parameters: Users who for whatever reason might have created their own struct and are using them as IEnumerable parameters are going to get failures in AOT scenarios. We can't dynamically create those arrays. --- .../Annotations/CommandArgumentAttribute.cs | 14 +- .../Annotations/CommandOptionAttribute.cs | 14 +- .../Annotations/PairDeconstructorAttribute.cs | 8 +- src/Spectre.Console.Cli/AsyncCommandOfT.cs | 2 +- src/Spectre.Console.Cli/CommandApp.cs | 5 +- src/Spectre.Console.Cli/CommandAppOfT.cs | 5 +- src/Spectre.Console.Cli/CommandOfT.cs | 2 +- .../ConfiguratorExtensions.cs | 2 +- .../CreateInstanceHelpers.cs | 204 ++++++++++++++++++ src/Spectre.Console.Cli/ICommandOfT.cs | 2 +- .../ICommandParameterInfo.cs | 1 + src/Spectre.Console.Cli/IConfigurator.cs | 4 +- src/Spectre.Console.Cli/IConfiguratorOfT.cs | 4 +- src/Spectre.Console.Cli/ITypeRegistrar.cs | 2 +- .../ITypeRegistrarFrontend.cs | 2 +- .../Binding/CommandConstructorBinder.cs | 8 +- .../Internal/Binding/CommandPropertyBinder.cs | 7 +- .../Internal/Binding/CommandValueBinder.cs | 6 +- .../Internal/Binding/CommandValueResolver.cs | 181 ++++++++++++++-- .../Internal/Collections/MultiMap.cs | 11 + .../Internal/CommandBinder.cs | 5 +- .../Internal/Composition/Activators.cs | 6 +- .../Composition/DefaultTypeRegistrar.cs | 2 +- .../Composition/DefaultTypeResolver.cs | 7 +- .../Configuration/ConfigurationHelper.cs | 4 +- .../Internal/Configuration/Configurator.cs | 9 +- .../Internal/Configuration/ConfiguratorOfT.cs | 7 +- .../Configuration/ConfiguredCommand.cs | 10 +- .../Internal/DefaultPairDeconstructor.cs | 17 +- .../Internal/Modelling/CommandInfo.cs | 15 +- .../Internal/Modelling/CommandModel.cs | 2 +- .../Internal/Modelling/CommandParameter.cs | 20 +- .../Modelling/CommandParameterComparer.cs | 4 +- .../Internal/TypeRegistrar.cs | 2 +- .../Internal/TypeResolverAdapter.cs | 7 +- src/Spectre.Console.Cli/PairDeconstructor.cs | 4 +- src/Spectre.Console.Cli/Properties/Usings.cs | 3 +- .../Spectre.Console.Cli.csproj | 5 +- .../Unsafe/IUnsafeConfigurator.cs | 3 +- .../Spectre.Console.Testing.csproj | 1 + src/Spectre.Console.sln | 15 ++ .../Internal/TypeConverterHelper.cs | 8 +- .../Commands/Add/AddPackageCommand.cs | 46 ++++ .../Commands/Add/AddReferenceCommand.cs | 29 +++ .../Commands/Add/AddSettings.cs | 11 + .../Commands/Run/RunCommand.cs | 72 +++++++ .../Commands/Serve/ServeCommand.cs | 39 ++++ .../Spectre.Console.TrimTest/MyInterceptor.cs | 20 ++ src/Tests/Spectre.Console.TrimTest/Program.cs | 33 +++ .../Spectre.Console.TrimTest.csproj | 22 ++ .../Utilities/SettingsDumper.cs | 27 +++ .../Spectre.Console.TrimTest/Verbosity.cs | 51 +++++ 52 files changed, 889 insertions(+), 101 deletions(-) create mode 100644 src/Spectre.Console.Cli/CreateInstanceHelpers.cs create mode 100644 src/Tests/Spectre.Console.TrimTest/Commands/Add/AddPackageCommand.cs create mode 100644 src/Tests/Spectre.Console.TrimTest/Commands/Add/AddReferenceCommand.cs create mode 100644 src/Tests/Spectre.Console.TrimTest/Commands/Add/AddSettings.cs create mode 100644 src/Tests/Spectre.Console.TrimTest/Commands/Run/RunCommand.cs create mode 100644 src/Tests/Spectre.Console.TrimTest/Commands/Serve/ServeCommand.cs create mode 100644 src/Tests/Spectre.Console.TrimTest/MyInterceptor.cs create mode 100644 src/Tests/Spectre.Console.TrimTest/Program.cs create mode 100644 src/Tests/Spectre.Console.TrimTest/Spectre.Console.TrimTest.csproj create mode 100644 src/Tests/Spectre.Console.TrimTest/Utilities/SettingsDumper.cs create mode 100644 src/Tests/Spectre.Console.TrimTest/Verbosity.cs diff --git a/src/Spectre.Console.Cli/Annotations/CommandArgumentAttribute.cs b/src/Spectre.Console.Cli/Annotations/CommandArgumentAttribute.cs index a3716059a..2fd3e5bf1 100644 --- a/src/Spectre.Console.Cli/Annotations/CommandArgumentAttribute.cs +++ b/src/Spectre.Console.Cli/Annotations/CommandArgumentAttribute.cs @@ -7,6 +7,9 @@ namespace Spectre.Console.Cli; [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] public sealed class CommandArgumentAttribute : Attribute { + [DynamicallyAccessedMembers(PublicConstructors)] + internal Type? ArgumentType { get; } + /// /// Gets the argument position. /// @@ -32,7 +35,8 @@ public sealed class CommandArgumentAttribute : Attribute /// /// The argument position. /// The argument template. Wrap in <> for required arguments, [] for optional ones. For example "[MyArgument]". - public CommandArgumentAttribute(int position, string template) + /// The type of the parameter. Required for AOT scenarios. + public CommandArgumentAttribute(int position, string template, [DynamicallyAccessedMembers(PublicConstructors)] Type? argumentType = null) { if (template == null) { @@ -46,5 +50,13 @@ public CommandArgumentAttribute(int position, string template) Position = position; ValueName = result.Value; IsRequired = result.Required; + + // if someone was explicit about the type of option, then we need to register an + // explicit builder for this type to be used. + ArgumentType = argumentType; + if (ArgumentType != null) + { + CreateInstanceHelpers.RegisterNewInstanceBuilder(ArgumentType); + } } } \ No newline at end of file diff --git a/src/Spectre.Console.Cli/Annotations/CommandOptionAttribute.cs b/src/Spectre.Console.Cli/Annotations/CommandOptionAttribute.cs index 098c3f464..9829218d4 100644 --- a/src/Spectre.Console.Cli/Annotations/CommandOptionAttribute.cs +++ b/src/Spectre.Console.Cli/Annotations/CommandOptionAttribute.cs @@ -7,6 +7,9 @@ namespace Spectre.Console.Cli; [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] public sealed class CommandOptionAttribute : Attribute { + [DynamicallyAccessedMembers(PublicConstructors)] + internal Type? OptionType { get; } + /// /// Gets the long names of the option. /// @@ -39,7 +42,8 @@ public sealed class CommandOptionAttribute : Attribute /// Initializes a new instance of the class. /// /// The option template. - public CommandOptionAttribute(string template) + /// The type of the parameter. Required for AOT scenarios. + public CommandOptionAttribute(string template, [DynamicallyAccessedMembers(PublicConstructors)] Type? optionType = null) { if (template == null) { @@ -54,6 +58,14 @@ public CommandOptionAttribute(string template) ShortNames = result.ShortNames; ValueName = result.Value; ValueIsOptional = result.ValueIsOptional; + + // if someone was explicit about the type of option, then we need to register an + // explicit builder for this type to be used. + OptionType = optionType; + if (OptionType != null) + { + CreateInstanceHelpers.RegisterNewInstanceBuilder(OptionType); + } } internal bool IsMatch(string name) diff --git a/src/Spectre.Console.Cli/Annotations/PairDeconstructorAttribute.cs b/src/Spectre.Console.Cli/Annotations/PairDeconstructorAttribute.cs index b21260271..bde38265b 100644 --- a/src/Spectre.Console.Cli/Annotations/PairDeconstructorAttribute.cs +++ b/src/Spectre.Console.Cli/Annotations/PairDeconstructorAttribute.cs @@ -12,7 +12,11 @@ public sealed class PairDeconstructorAttribute : Attribute /// pair deconstructor class to use for data conversion for the /// object this attribute is bound to. /// - public Type Type { get; } + public Type Type + { + [return: DynamicallyAccessedMembers(PublicConstructors | PublicProperties)] + get; + } /// /// Initializes a new instance of the class. @@ -21,7 +25,7 @@ public sealed class PairDeconstructorAttribute : Attribute /// A System.Type that represents the type of the pair deconstructor /// class to use for data conversion for the object this attribute is bound to. /// - public PairDeconstructorAttribute(Type type) + public PairDeconstructorAttribute([DynamicallyAccessedMembers(PublicConstructors | PublicProperties)] Type type) { Type = type ?? throw new ArgumentNullException(nameof(type)); } diff --git a/src/Spectre.Console.Cli/AsyncCommandOfT.cs b/src/Spectre.Console.Cli/AsyncCommandOfT.cs index 87da022a0..39ceda6f9 100644 --- a/src/Spectre.Console.Cli/AsyncCommandOfT.cs +++ b/src/Spectre.Console.Cli/AsyncCommandOfT.cs @@ -4,7 +4,7 @@ namespace Spectre.Console.Cli; /// Base class for an asynchronous command. /// /// The settings type. -public abstract class AsyncCommand : ICommand +public abstract class AsyncCommand<[DynamicallyAccessedMembers(PublicConstructors)] TSettings> : ICommand where TSettings : CommandSettings { /// diff --git a/src/Spectre.Console.Cli/CommandApp.cs b/src/Spectre.Console.Cli/CommandApp.cs index cc6712db5..8f971dab7 100644 --- a/src/Spectre.Console.Cli/CommandApp.cs +++ b/src/Spectre.Console.Cli/CommandApp.cs @@ -5,9 +5,6 @@ namespace Spectre.Console.Cli; /// /// The entry point for a command line application. /// -#if !NETSTANDARD2_0 -[RequiresDynamicCode("Spectre.Console.Cli relies on reflection. Use during trimming and AOT compilation is not supported and may result in unexpected behaviors.")] -#endif public sealed class CommandApp : ICommandApp { private readonly Configurator _configurator; @@ -45,7 +42,7 @@ public void Configure(Action configuration) /// /// The command type. /// A that can be used to configure the default command. - public DefaultCommandConfigurator SetDefaultCommand() + public DefaultCommandConfigurator SetDefaultCommand<[DynamicallyAccessedMembers(PublicConstructors | Interfaces)] TCommand>() where TCommand : class, ICommand { return new DefaultCommandConfigurator(GetConfigurator().SetDefaultCommand()); diff --git a/src/Spectre.Console.Cli/CommandAppOfT.cs b/src/Spectre.Console.Cli/CommandAppOfT.cs index 6df8539ff..4bb640077 100644 --- a/src/Spectre.Console.Cli/CommandAppOfT.cs +++ b/src/Spectre.Console.Cli/CommandAppOfT.cs @@ -6,10 +6,7 @@ namespace Spectre.Console.Cli; /// The entry point for a command line application with a default command. /// /// The type of the default command. -#if !NETSTANDARD2_0 -[RequiresDynamicCode("Spectre.Console.Cli relies on reflection. Use during trimming and AOT compilation is not supported and may result in unexpected behaviors.")] -#endif -public sealed class CommandApp : ICommandApp +public sealed class CommandApp<[DynamicallyAccessedMembers(PublicConstructors | Interfaces)] TDefaultCommand> : ICommandApp where TDefaultCommand : class, ICommand { private readonly CommandApp _app; diff --git a/src/Spectre.Console.Cli/CommandOfT.cs b/src/Spectre.Console.Cli/CommandOfT.cs index e2be58ef1..457ca0c3f 100644 --- a/src/Spectre.Console.Cli/CommandOfT.cs +++ b/src/Spectre.Console.Cli/CommandOfT.cs @@ -5,7 +5,7 @@ namespace Spectre.Console.Cli; /// /// The settings type. /// -public abstract class Command : ICommand +public abstract class Command<[DynamicallyAccessedMembers(PublicConstructors)] TSettings> : ICommand where TSettings : CommandSettings { /// diff --git a/src/Spectre.Console.Cli/ConfiguratorExtensions.cs b/src/Spectre.Console.Cli/ConfiguratorExtensions.cs index 80895e302..3b0c00529 100644 --- a/src/Spectre.Console.Cli/ConfiguratorExtensions.cs +++ b/src/Spectre.Console.Cli/ConfiguratorExtensions.cs @@ -29,7 +29,7 @@ public static IConfigurator SetHelpProvider(this IConfigurator configurator, IHe /// The configurator. /// The type of the help provider to instantiate at runtime and use. /// A configurator that can be used to configure the application further. - public static IConfigurator SetHelpProvider(this IConfigurator configurator) + public static IConfigurator SetHelpProvider<[DynamicallyAccessedMembers(PublicConstructors | PublicProperties)] T>(this IConfigurator configurator) where T : IHelpProvider { if (configurator == null) diff --git a/src/Spectre.Console.Cli/CreateInstanceHelpers.cs b/src/Spectre.Console.Cli/CreateInstanceHelpers.cs new file mode 100644 index 000000000..51ec9324e --- /dev/null +++ b/src/Spectre.Console.Cli/CreateInstanceHelpers.cs @@ -0,0 +1,204 @@ +using System.Runtime.CompilerServices; + +namespace Spectre.Console.Cli; + +internal static class CreateInstanceHelpers +{ + private static readonly Dictionary> _arrayBuilder; + private static readonly Dictionary> _multiMapBuilder; + private static readonly Dictionary> _instanceBuilder = new(); + + static CreateInstanceHelpers() + { + _arrayBuilder = new Dictionary> + { + [typeof(bool)] = size => new bool[size], + [typeof(byte)] = size => new byte[size], + [typeof(sbyte)] = size => new sbyte[size], + [typeof(char)] = size => new char[size], + [typeof(double)] = size => new double[size], + [typeof(string)] = size => new string[size], + [typeof(int)] = size => new int[size], + [typeof(short)] = size => new short[size], + [typeof(long)] = size => new long[size], + [typeof(float)] = size => new float[size], + [typeof(ushort)] = size => new ushort[size], + [typeof(uint)] = size => new uint[size], + [typeof(ulong)] = size => new ulong[size], + [typeof(DateTime)] = size => new DateTime[size], + [typeof(DateTimeOffset)] = size => new DateTimeOffset[size], + [typeof(decimal)] = size => new decimal[size], + [typeof(TimeSpan)] = size => new TimeSpan[size], + [typeof(Guid)] = size => new Guid[size], +#if !NETSTANDARD2_0 + [typeof(Int128)] = size => new Int128[size], + [typeof(Half)] = size => new Half[size], + [typeof(UInt128)] = size => new UInt128[size], + [typeof(DateOnly)] = size => new DateOnly[size], + [typeof(TimeOnly)] = size => new TimeOnly[size], +#endif + }; + + _multiMapBuilder = new Dictionary> + { + [typeof(bool)] = () => new MultiMap(), + [typeof(bool)] = () => new MultiMap(), + [typeof(byte)] = () => new MultiMap(), + [typeof(sbyte)] = () => new MultiMap(), + [typeof(char)] = () => new MultiMap(), + [typeof(double)] = () => new MultiMap(), + [typeof(string)] = () => new MultiMap(), + [typeof(int)] = () => new MultiMap(), + [typeof(short)] = () => new MultiMap(), + [typeof(long)] = () => new MultiMap(), + [typeof(float)] = () => new MultiMap(), + [typeof(ushort)] = () => new MultiMap(), + [typeof(uint)] = () => new MultiMap(), + [typeof(ulong)] = () => new MultiMap(), + [typeof(DateTime)] = () => new MultiMap(), + [typeof(DateTimeOffset)] = () => new MultiMap(), + [typeof(decimal)] = () => new MultiMap(), + [typeof(TimeSpan)] = () => new MultiMap(), + [typeof(Guid)] = () => new MultiMap(), +#if !NETSTANDARD2_0 + [typeof(Int128)] = () => new MultiMap(), + [typeof(Half)] = () => new MultiMap(), + [typeof(UInt128)] = () => new MultiMap(), + [typeof(DateOnly)] = () => new MultiMap(), + [typeof(TimeOnly)] = () => new MultiMap(), +#endif + }; + } + + /// + /// Add a new known type instance builder. + /// + /// The type to build. + public static void RegisterNewInstanceBuilder([DynamicallyAccessedMembers(PublicConstructors)] Type type) + { + _instanceBuilder.Add(type, input => + { + var constructor = type.GetConstructor(BindingFlags.Public | BindingFlags.Instance, null, input.Select(i => i.GetType()).ToArray(), null); + if (constructor == null) + { + throw new InvalidOperationException("Could not create single parameter instance."); + } + + return constructor.Invoke(input); + }); + } + + public static bool TryGetInstance(Type type, object[] parameters, [NotNullWhen(true)] out object? result) + { + if (_instanceBuilder.TryGetValue(type, out var factory)) + { + result = factory(parameters); + return true; + } + + if (CanDoCreateInstance) + { + result = BuildInstance(type, parameters); + if (result != null) + { + return true; + } + } + + result = null; + return false; + } + + private static object? BuildInstance(Type type, object[] parameters) + { + if (CanDoUnreferencedCode) + { + var constructor = type.GetConstructor(BindingFlags.Public | BindingFlags.Instance, null, parameters.Select(i => i.GetType()).ToArray(), null); + if (constructor == null) + { + throw new InvalidOperationException("Could not create single parameter instance."); + } + + return constructor.Invoke(parameters); + } + + return null; + } + + public static Array CreateArrayInstance(Type type, int size) + { +#if NET9_0_OR_GREATER + return Array.CreateInstanceFromArrayType(type, size); +#else + var elementType = type.GetElementType(); + if (elementType == null) + { + throw new InvalidOperationException("Could not create an array of type " + type.FullName + "."); + } + + return CreateArrayInstanceFromElementType(elementType, size); +#endif + } + + public static IMultiMap? CreateMultiMapInstance(Type type1, Type type2) + { + if (type1 == typeof(string)) + { + if (_multiMapBuilder.TryGetValue(type2, out var multiMapBuilder)) + { + return multiMapBuilder.Invoke(); + } + } + + if (CanDoCreateInstance) + { + return Activator.CreateInstance(typeof(MultiMap<,>).MakeGenericType(type1, type2)) as IMultiMap; + } + + throw new InvalidOperationException("Could not create a multi map of type " + type1.FullName + " and " + type2.FullName + ". If you are running in AOT, only dictionaries with a string key and an .NET primitive value are supported."); + } + + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "We are only creating an array instance dynamically for non-value types not in our dictionary.")] + public static Array CreateArrayInstanceFromElementType(Type getType, int sourceArrayLength) + { + if (_arrayBuilder.TryGetValue(getType, out var value)) + { + return value(sourceArrayLength); + } + + if (!getType.IsValueType || CanDoCreateInstance) + { + return Array.CreateInstance(getType, sourceArrayLength); + } + + throw new InvalidOperationException("Cannot create array instance of type " + getType.FullName + ". "); + } + + [FeatureGuard(typeof(RequiresUnreferencedCodeAttribute))] + public static bool CanDoUnreferencedCode + { + get + { +#if NET9_0_OR_GREATER +#pragma warning disable IL4000 + return RuntimeFeature.IsDynamicCodeSupported; +#pragma warning restore IL4000 +#else + return true; +#endif + } + } + + [FeatureGuard(typeof(RequiresDynamicCodeAttribute))] + public static bool CanDoCreateInstance + { + get + { +#if NET9_0_OR_GREATER + return RuntimeFeature.IsDynamicCodeSupported; +#else + return true; +#endif + } + } +} \ No newline at end of file diff --git a/src/Spectre.Console.Cli/ICommandOfT.cs b/src/Spectre.Console.Cli/ICommandOfT.cs index 5b92cf8be..f8a931567 100644 --- a/src/Spectre.Console.Cli/ICommandOfT.cs +++ b/src/Spectre.Console.Cli/ICommandOfT.cs @@ -4,7 +4,7 @@ namespace Spectre.Console.Cli; /// Represents a command. /// /// The settings type. -public interface ICommand : ICommandLimiter +public interface ICommand<[DynamicallyAccessedMembers(PublicConstructors)] TSettings> : ICommandLimiter where TSettings : CommandSettings { /// diff --git a/src/Spectre.Console.Cli/ICommandParameterInfo.cs b/src/Spectre.Console.Cli/ICommandParameterInfo.cs index b6483eff4..26a264607 100644 --- a/src/Spectre.Console.Cli/ICommandParameterInfo.cs +++ b/src/Spectre.Console.Cli/ICommandParameterInfo.cs @@ -14,6 +14,7 @@ public interface ICommandParameterInfo /// /// Gets the parameter type. /// + [DynamicallyAccessedMembers(PublicConstructors | Interfaces)] public Type ParameterType { get; } /// diff --git a/src/Spectre.Console.Cli/IConfigurator.cs b/src/Spectre.Console.Cli/IConfigurator.cs index ccac8d025..df396c1c8 100644 --- a/src/Spectre.Console.Cli/IConfigurator.cs +++ b/src/Spectre.Console.Cli/IConfigurator.cs @@ -15,7 +15,7 @@ public interface IConfigurator /// Sets the help provider for the application. /// /// The type of the help provider to instantiate at runtime and use. - public void SetHelpProvider() + public void SetHelpProvider<[DynamicallyAccessedMembers(PublicConstructors | PublicProperties)] T>() where T : IHelpProvider; /// @@ -35,7 +35,7 @@ public void SetHelpProvider() /// The command type. /// The name of the command. /// A command configurator that can be used to configure the command further. - ICommandConfigurator AddCommand(string name) + ICommandConfigurator AddCommand<[DynamicallyAccessedMembers(PublicConstructors | Interfaces)] TCommand>(string name) where TCommand : class, ICommand; /// diff --git a/src/Spectre.Console.Cli/IConfiguratorOfT.cs b/src/Spectre.Console.Cli/IConfiguratorOfT.cs index 0c71b348c..35c7cb9fa 100644 --- a/src/Spectre.Console.Cli/IConfiguratorOfT.cs +++ b/src/Spectre.Console.Cli/IConfiguratorOfT.cs @@ -28,7 +28,7 @@ public interface IConfigurator /// arguments, flags or option values. /// /// The default command type. - void SetDefaultCommand() + void SetDefaultCommand<[DynamicallyAccessedMembers(PublicConstructors | Interfaces)] TDefaultCommand>() where TDefaultCommand : class, ICommandLimiter; /// @@ -44,7 +44,7 @@ void SetDefaultCommand() /// The command type. /// The name of the command. /// A command configurator that can be used to configure the command further. - ICommandConfigurator AddCommand(string name) + ICommandConfigurator AddCommand<[DynamicallyAccessedMembers(PublicConstructors | Interfaces)] TCommand>(string name) where TCommand : class, ICommandLimiter; /// diff --git a/src/Spectre.Console.Cli/ITypeRegistrar.cs b/src/Spectre.Console.Cli/ITypeRegistrar.cs index 0feea7e3f..78f2ea949 100644 --- a/src/Spectre.Console.Cli/ITypeRegistrar.cs +++ b/src/Spectre.Console.Cli/ITypeRegistrar.cs @@ -10,7 +10,7 @@ public interface ITypeRegistrar /// /// The service. /// The implementation. - void Register(Type service, Type implementation); + void Register(Type service, [DynamicallyAccessedMembers(PublicConstructors)] Type implementation); /// /// Registers the specified instance. diff --git a/src/Spectre.Console.Cli/ITypeRegistrarFrontend.cs b/src/Spectre.Console.Cli/ITypeRegistrarFrontend.cs index dbe955e2e..7625fa515 100644 --- a/src/Spectre.Console.Cli/ITypeRegistrarFrontend.cs +++ b/src/Spectre.Console.Cli/ITypeRegistrarFrontend.cs @@ -10,7 +10,7 @@ public interface ITypeRegistrarFrontend /// /// The exposed service type. /// The implementing type. - void Register() + void Register() where TImplementation : TService; /// diff --git a/src/Spectre.Console.Cli/Internal/Binding/CommandConstructorBinder.cs b/src/Spectre.Console.Cli/Internal/Binding/CommandConstructorBinder.cs index 83c877e30..7e478c137 100644 --- a/src/Spectre.Console.Cli/Internal/Binding/CommandConstructorBinder.cs +++ b/src/Spectre.Console.Cli/Internal/Binding/CommandConstructorBinder.cs @@ -2,7 +2,11 @@ namespace Spectre.Console.Cli; internal static class CommandConstructorBinder { - public static CommandSettings CreateSettings(CommandValueLookup lookup, ConstructorInfo constructor, ITypeResolver resolver) + public static CommandSettings CreateSettings( + CommandValueLookup lookup, + [DynamicallyAccessedMembers(PublicConstructors | PublicProperties)] Type settingsType, + ConstructorInfo constructor, + ITypeResolver resolver) { if (constructor.DeclaringType == null) { @@ -31,7 +35,7 @@ public static CommandSettings CreateSettings(CommandValueLookup lookup, Construc } // Create the settings. - if (!(Activator.CreateInstance(constructor.DeclaringType, parameters.ToArray()) is CommandSettings settings)) + if (!(Activator.CreateInstance(settingsType, parameters.ToArray()) is CommandSettings settings)) { throw new InvalidOperationException("Could not create settings"); } diff --git a/src/Spectre.Console.Cli/Internal/Binding/CommandPropertyBinder.cs b/src/Spectre.Console.Cli/Internal/Binding/CommandPropertyBinder.cs index 31cbf5b0d..863e0ab5c 100644 --- a/src/Spectre.Console.Cli/Internal/Binding/CommandPropertyBinder.cs +++ b/src/Spectre.Console.Cli/Internal/Binding/CommandPropertyBinder.cs @@ -2,7 +2,8 @@ namespace Spectre.Console.Cli; internal static class CommandPropertyBinder { - public static CommandSettings CreateSettings(CommandValueLookup lookup, Type settingsType, ITypeResolver resolver) + public static CommandSettings CreateSettings(CommandValueLookup lookup, [DynamicallyAccessedMembers(PublicConstructors | PublicProperties)] + Type settingsType, ITypeResolver resolver) { var settings = CreateSettings(resolver, settingsType); @@ -24,7 +25,9 @@ public static CommandSettings CreateSettings(CommandValueLookup lookup, Type set return settings; } - private static CommandSettings CreateSettings(ITypeResolver resolver, Type settingsType) + private static CommandSettings CreateSettings( + ITypeResolver resolver, + [DynamicallyAccessedMembers(PublicConstructors | PublicProperties)] Type settingsType) { if (resolver.Resolve(settingsType) is CommandSettings settings) { diff --git a/src/Spectre.Console.Cli/Internal/Binding/CommandValueBinder.cs b/src/Spectre.Console.Cli/Internal/Binding/CommandValueBinder.cs index 8dfbee45d..b862afc64 100644 --- a/src/Spectre.Console.Cli/Internal/Binding/CommandValueBinder.cs +++ b/src/Spectre.Console.Cli/Internal/Binding/CommandValueBinder.cs @@ -34,7 +34,7 @@ private object GetLookup(CommandParameter parameter, ITypeResolver resolver, obj var multimap = (IMultiMap?)_lookup.GetValue(parameter); if (multimap == null) { - multimap = Activator.CreateInstance(typeof(MultiMap<,>).MakeGenericType(genericTypes[0], genericTypes[1])) as IMultiMap; + multimap = CreateInstanceHelpers.CreateMultiMapInstance(genericTypes[0], genericTypes[1]); if (multimap == null) { throw new InvalidOperationException("Could not create multimap"); @@ -82,11 +82,11 @@ private object GetArray(CommandParameter parameter, object? value) if (array == null) { - newArray = Array.CreateInstance(elementType, 1); + newArray = CreateInstanceHelpers.CreateArrayInstance(parameter.Property.PropertyType, 1); } else { - newArray = Array.CreateInstance(elementType, array.Length + 1); + newArray = CreateInstanceHelpers.CreateArrayInstance(parameter.Property.PropertyType, array.Length + 1); array.CopyTo(newArray, 0); } diff --git a/src/Spectre.Console.Cli/Internal/Binding/CommandValueResolver.cs b/src/Spectre.Console.Cli/Internal/Binding/CommandValueResolver.cs index 4d817569f..8510fd10f 100644 --- a/src/Spectre.Console.Cli/Internal/Binding/CommandValueResolver.cs +++ b/src/Spectre.Console.Cli/Internal/Binding/CommandValueResolver.cs @@ -114,7 +114,8 @@ public static CommandValueLookup GetParameterValues(CommandTree? tree, ITypeReso return lookup; } - private static object? ConvertValue(ITypeResolver resolver, CommandValueLookup lookup, CommandValueBinder binder, CommandParameter parameter, object? result) + private static object? ConvertValue(ITypeResolver resolver, CommandValueLookup lookup, CommandValueBinder binder, + CommandParameter parameter, object? result) { if (result != null && result.GetType() != parameter.ParameterType) { @@ -128,6 +129,7 @@ public static CommandValueLookup GetParameterValues(CommandTree? tree, ITypeReso private static Array ConvertArray(Array sourceArray, SmartConverter converter) { Array? targetArray = null; + for (var i = 0; i < sourceArray.Length; i++) { var item = sourceArray.GetValue(i); @@ -136,7 +138,7 @@ private static Array ConvertArray(Array sourceArray, SmartConverter converter) var converted = converter.ConvertFrom(item); if (converted != null) { - targetArray ??= Array.CreateInstance(converted.GetType(), sourceArray.Length); + targetArray ??= CreateInstanceHelpers.CreateArrayInstanceFromElementType(converted.GetType(), sourceArray.Length); targetArray.SetValue(converted, i); } } @@ -146,7 +148,8 @@ private static Array ConvertArray(Array sourceArray, SmartConverter converter) } [SuppressMessage("Style", "IDE0019:Use pattern matching", Justification = "It's OK")] - private static SmartConverter GetConverter(CommandValueLookup lookup, CommandValueBinder binder, ITypeResolver resolver, CommandParameter parameter) + private static SmartConverter GetConverter(CommandValueLookup lookup, CommandValueBinder binder, + ITypeResolver resolver, CommandParameter parameter) { if (parameter.Converter == null) { @@ -159,7 +162,7 @@ private static SmartConverter GetConverter(CommandValueLookup lookup, CommandVal throw new InvalidOperationException("Could not get element type"); } - return new SmartConverter(TypeDescriptor.GetConverter(elementType), elementType); + return new SmartConverter(elementType); } if (parameter.IsFlagValue()) @@ -179,10 +182,10 @@ private static SmartConverter GetConverter(CommandValueLookup lookup, CommandVal } // Return a converter for the flag element type. - return new SmartConverter(TypeDescriptor.GetConverter(value.Type), value.Type); + return new SmartConverter(value.Type); } - return new SmartConverter(TypeDescriptor.GetConverter(parameter.ParameterType), parameter.ParameterType); + return new SmartConverter(parameter.ParameterType); } var type = Type.GetType(parameter.Converter.ConverterTypeName); @@ -205,6 +208,12 @@ public SmartConverter(TypeConverter typeConverter, Type type) Type = type; } + public SmartConverter(Type type) + { + Type = type; + TypeConverter = TypeConverterHelper.GetTypeConverter(type); + } + public TypeConverter TypeConverter { get; } private Type Type { get; } @@ -216,14 +225,162 @@ public SmartConverter(TypeConverter typeConverter, Type type) } catch (NotSupportedException) { - var constructor = Type.GetConstructor(BindingFlags.Public | BindingFlags.Instance, null, new[] { input.GetType() }, null); - if (constructor == null) + return CreateSingleParameterInstance(input); + } + } + + private object? CreateSingleParameterInstance(object input) + { + if (CreateInstanceHelpers.TryGetInstance(Type, [input], out var instance)) + { + return instance; + } + + throw new InvalidOperationException("Could not create single parameter instance."); + } + } +} + +internal static class TypeConverterHelper + { + public static TypeConverter GetTypeConverter(Type type) + { + var converter = GetConverter(type); + if (converter != null) + { + return converter; + } + + var attribute = type.GetCustomAttribute(); + if (attribute != null) + { + var converterType = Type.GetType(attribute.ConverterTypeName, false, false); + if (converterType != null) { - throw; + converter = Activator.CreateInstance(converterType) as TypeConverter; + if (converter != null) + { + return converter; + } } + } + + throw new InvalidOperationException("Could not find type converter"); - return constructor.Invoke(new[] { input }); + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2067", + Justification = "Feature switches are not currently supported in the analyzer")] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", + Justification = "Feature switches are not currently supported in the analyzer")] + static TypeConverter? GetConverter(Type type) + { + // otherwise try and use the intrinsic converter. if we can't find one, then + // try and use GetConverter. + var intrinsicConverter = GetIntrinsicConverter(type); + return intrinsicConverter ?? TypeDescriptor.GetConverter(type); } } - } -} \ No newline at end of file + + private static readonly Dictionary> _intrinsicConverters; + private static readonly Dictionary _defaultValuesAsString; + + static TypeConverterHelper() + { + _intrinsicConverters = new Dictionary> + { + [typeof(bool)] = _ => new BooleanConverter(), + [typeof(byte)] = _ => new ByteConverter(), + [typeof(sbyte)] = _ => new SByteConverter(), + [typeof(char)] = _ => new CharConverter(), + [typeof(double)] = _ => new DoubleConverter(), + [typeof(string)] = _ => new StringConverter(), + [typeof(int)] = _ => new Int32Converter(), + [typeof(short)] = _ => new Int16Converter(), + [typeof(long)] = _ => new Int64Converter(), + [typeof(float)] = _ => new SingleConverter(), + [typeof(ushort)] = _ => new UInt16Converter(), + [typeof(uint)] = _ => new UInt32Converter(), + [typeof(ulong)] = _ => new UInt64Converter(), + [typeof(object)] = _ => new TypeConverter(), + [typeof(CultureInfo)] = _ => new CultureInfoConverter(), + [typeof(DateTime)] = _ => new DateTimeConverter(), + [typeof(DateTimeOffset)] = _ => new DateTimeOffsetConverter(), + [typeof(decimal)] = _ => new DecimalConverter(), + [typeof(TimeSpan)] = _ => new TimeSpanConverter(), + [typeof(Guid)] = _ => new GuidConverter(), + [typeof(Uri)] = _ => new UriTypeConverter(), + [typeof(Array)] = _ => new ArrayConverter(), + [typeof(ICollection)] = _ => new CollectionConverter(), + [typeof(Enum)] = CreateEnumConverter(), +#if !NETSTANDARD2_0 + [typeof(Int128)] = _ => new Int128Converter(), + [typeof(Half)] = _ => new HalfConverter(), + [typeof(UInt128)] = _ => new UInt128Converter(), + [typeof(DateOnly)] = _ => new DateOnlyConverter(), + [typeof(TimeOnly)] = _ => new TimeOnlyConverter(), + [typeof(Version)] = _ => new VersionConverter(), +#endif + }; + + _defaultValuesAsString = new Dictionary + { + [typeof(bool)] = default(bool).ToString(), + [typeof(byte)] = default(byte).ToString(), + [typeof(sbyte)] = default(sbyte).ToString(), + [typeof(char)] = default(char).ToString(), + [typeof(double)] = default(double).ToString(CultureInfo.CurrentCulture), + [typeof(string)] = string.Empty, + [typeof(int)] = default(int).ToString(), + [typeof(short)] = default(short).ToString(), + [typeof(long)] = default(long).ToString(), + [typeof(float)] = default(float).ToString(CultureInfo.CurrentCulture), + [typeof(ushort)] = default(ushort).ToString(), + [typeof(uint)] = default(uint).ToString(), + [typeof(ulong)] = default(ulong).ToString(), + [typeof(DateTime)] = default(DateTime).ToString(CultureInfo.CurrentCulture), + [typeof(DateTimeOffset)] = default(DateTimeOffset).ToString(CultureInfo.CurrentCulture), + [typeof(decimal)] = default(decimal).ToString(CultureInfo.CurrentCulture), + [typeof(TimeSpan)] = default(TimeSpan).ToString(), + [typeof(Guid)] = default(Guid).ToString(), +#if !NETSTANDARD2_0 + [typeof(Int128)] = default(Int128).ToString(), + [typeof(Half)] = default(Half).ToString(), + [typeof(UInt128)] = default(UInt128).ToString(), + [typeof(DateOnly)] = default(DateOnly).ToString(), + [typeof(TimeOnly)] = default(TimeOnly).ToString(), +#endif + }; + } + + public static string GetDefaultValueOfType(Type type) => _defaultValuesAsString[type]; + + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2111", Justification = "Delegate reflection is safe for all usages in this type.")] + private static Func CreateEnumConverter() => ([DynamicallyAccessedMembers(PublicConstructors | PublicFields)] type) => new EnumConverter(type); + + /// + /// A highly-constrained version of that only returns intrinsic converters. + /// + private static TypeConverter? GetIntrinsicConverter([DynamicallyAccessedMembers(PublicConstructors | PublicFields)] Type type) + { + if (type.IsArray) + { + type = typeof(Array); + } + + if (typeof(ICollection).IsAssignableFrom(type)) + { + type = typeof(ICollection); + } + + if (type.IsEnum) + { + return new EnumConverter(type); + } + + if (_intrinsicConverters.TryGetValue(type, out var factory)) + { + return factory(type); + } + + return null; + } + } \ No newline at end of file diff --git a/src/Spectre.Console.Cli/Internal/Collections/MultiMap.cs b/src/Spectre.Console.Cli/Internal/Collections/MultiMap.cs index 930970408..017663292 100644 --- a/src/Spectre.Console.Cli/Internal/Collections/MultiMap.cs +++ b/src/Spectre.Console.Cli/Internal/Collections/MultiMap.cs @@ -154,6 +154,17 @@ IEnumerator> IEnumerable>. return _dictionary.GetEnumerator(); } + public override string ToString() + { + var sb = new StringBuilder(); + foreach (var key in _dictionary.Keys) + { + sb.AppendLine($"{key}={_dictionary[key]}"); + } + + return sb.ToString(); + } + public void Add((object? Key, object? Value) pair) { if (pair.Key != null) diff --git a/src/Spectre.Console.Cli/Internal/CommandBinder.cs b/src/Spectre.Console.Cli/Internal/CommandBinder.cs index 69d83981c..2e5b95f67 100644 --- a/src/Spectre.Console.Cli/Internal/CommandBinder.cs +++ b/src/Spectre.Console.Cli/Internal/CommandBinder.cs @@ -2,7 +2,8 @@ namespace Spectre.Console.Cli; internal static class CommandBinder { - public static CommandSettings Bind(CommandTree? tree, Type settingsType, ITypeResolver resolver) + public static CommandSettings Bind(CommandTree? tree, [DynamicallyAccessedMembers(PublicConstructors | PublicProperties)] + Type settingsType, ITypeResolver resolver) { var lookup = CommandValueResolver.GetParameterValues(tree, resolver); @@ -17,7 +18,7 @@ public static CommandSettings Bind(CommandTree? tree, Type settingsType, ITypeRe if (lookup.HasParameterWithName(parameter?.Name)) { // Use constructor injection. - return CommandConstructorBinder.CreateSettings(lookup, constructor, resolver); + return CommandConstructorBinder.CreateSettings(lookup, settingsType, constructor, resolver); } } } diff --git a/src/Spectre.Console.Cli/Internal/Composition/Activators.cs b/src/Spectre.Console.Cli/Internal/Composition/Activators.cs index 9e8d06dec..c57bc5677 100644 --- a/src/Spectre.Console.Cli/Internal/Composition/Activators.cs +++ b/src/Spectre.Console.Cli/Internal/Composition/Activators.cs @@ -51,11 +51,12 @@ public override ComponentActivator CreateCopy() internal sealed class ReflectionActivator : ComponentActivator { + [DynamicallyAccessedMembers(PublicConstructors)] private readonly Type _type; private readonly ConstructorInfo _constructor; private readonly List _parameters; - public ReflectionActivator(Type type) + public ReflectionActivator([DynamicallyAccessedMembers(PublicConstructors)] Type type) { _type = type; _constructor = GetGreediestConstructor(type); @@ -104,7 +105,8 @@ public override ComponentActivator CreateCopy() return new ReflectionActivator(_type); } - private static ConstructorInfo GetGreediestConstructor(Type type) + private static ConstructorInfo GetGreediestConstructor([DynamicallyAccessedMembers(PublicConstructors)] + Type type) { ConstructorInfo? current = null; var count = -1; diff --git a/src/Spectre.Console.Cli/Internal/Composition/DefaultTypeRegistrar.cs b/src/Spectre.Console.Cli/Internal/Composition/DefaultTypeRegistrar.cs index 5672fca53..6e5184bec 100644 --- a/src/Spectre.Console.Cli/Internal/Composition/DefaultTypeRegistrar.cs +++ b/src/Spectre.Console.Cli/Internal/Composition/DefaultTypeRegistrar.cs @@ -21,7 +21,7 @@ public ITypeResolver Build() return container; } - public void Register(Type service, Type implementation) + public void Register(Type service, [DynamicallyAccessedMembers(PublicConstructors)] Type implementation) { var registration = new ComponentRegistration(implementation, new ReflectionActivator(implementation), new[] { service }); _registry.Enqueue(registry => registry.Register(registration)); diff --git a/src/Spectre.Console.Cli/Internal/Composition/DefaultTypeResolver.cs b/src/Spectre.Console.Cli/Internal/Composition/DefaultTypeResolver.cs index d59fbb3fb..8916517bb 100644 --- a/src/Spectre.Console.Cli/Internal/Composition/DefaultTypeResolver.cs +++ b/src/Spectre.Console.Cli/Internal/Composition/DefaultTypeResolver.cs @@ -41,7 +41,12 @@ public void Dispose() { if (isEnumerable) { - var result = Array.CreateInstance(type, registrations.Count); + if (type.IsValueType) + { + throw new InvalidOperationException($"The type {type.FullName} is a value type and not supported to be created as an array for dependency injection."); + } + + var result = CreateInstanceHelpers.CreateArrayInstanceFromElementType(type, registrations.Count); for (var index = 0; index < registrations.Count; index++) { var registration = registrations.ElementAt(index); diff --git a/src/Spectre.Console.Cli/Internal/Configuration/ConfigurationHelper.cs b/src/Spectre.Console.Cli/Internal/Configuration/ConfigurationHelper.cs index 405a6c8a8..bb0df2709 100644 --- a/src/Spectre.Console.Cli/Internal/Configuration/ConfigurationHelper.cs +++ b/src/Spectre.Console.Cli/Internal/Configuration/ConfigurationHelper.cs @@ -2,7 +2,7 @@ namespace Spectre.Console.Cli; internal static class ConfigurationHelper { - public static Type? GetSettingsType(Type commandType) + public static Type? GetSettingsType([DynamicallyAccessedMembers(Interfaces)] Type commandType) { if (typeof(ICommand).GetTypeInfo().IsAssignableFrom(commandType) && GetGenericTypeArguments(commandType, typeof(ICommand<>), out var result)) @@ -13,7 +13,7 @@ internal static class ConfigurationHelper return null; } - private static bool GetGenericTypeArguments(Type? type, Type genericType, + private static bool GetGenericTypeArguments([DynamicallyAccessedMembers(Interfaces)] Type? type, Type genericType, [NotNullWhen(true)] out Type[]? genericTypeArguments) { while (type != null) diff --git a/src/Spectre.Console.Cli/Internal/Configuration/Configurator.cs b/src/Spectre.Console.Cli/Internal/Configuration/Configurator.cs index 0bb22c813..1e2f1fc30 100644 --- a/src/Spectre.Console.Cli/Internal/Configuration/Configurator.cs +++ b/src/Spectre.Console.Cli/Internal/Configuration/Configurator.cs @@ -26,7 +26,7 @@ public void SetHelpProvider(IHelpProvider helpProvider) _registrar.RegisterInstance(typeof(IHelpProvider), helpProvider); } - public void SetHelpProvider() + public void SetHelpProvider<[DynamicallyAccessedMembers(PublicConstructors | PublicProperties)] T>() where T : IHelpProvider { // Register the help provider @@ -38,7 +38,7 @@ public void AddExample(params string[] args) Examples.Add(args); } - public ConfiguredCommand SetDefaultCommand() + public ConfiguredCommand SetDefaultCommand<[DynamicallyAccessedMembers(PublicConstructors | Interfaces)] TDefaultCommand>() where TDefaultCommand : class, ICommand { DefaultCommand = ConfiguredCommand.FromType( @@ -46,7 +46,7 @@ public ConfiguredCommand SetDefaultCommand() return DefaultCommand; } - public ICommandConfigurator AddCommand(string name) + public ICommandConfigurator AddCommand<[DynamicallyAccessedMembers(PublicConstructors | Interfaces)] TCommand>(string name) where TCommand : class, ICommand { var command = Commands.AddAndReturn(ConfiguredCommand.FromType(name, isDefaultCommand: false)); @@ -78,7 +78,7 @@ public IBranchConfigurator AddBranch(string name, Action action) { var command = ConfiguredCommand.FromBranch(settings, name); diff --git a/src/Spectre.Console.Cli/Internal/Configuration/ConfiguratorOfT.cs b/src/Spectre.Console.Cli/Internal/Configuration/ConfiguratorOfT.cs index 537e48540..a2b27b989 100644 --- a/src/Spectre.Console.Cli/Internal/Configuration/ConfiguratorOfT.cs +++ b/src/Spectre.Console.Cli/Internal/Configuration/ConfiguratorOfT.cs @@ -22,7 +22,7 @@ public void AddExample(params string[] args) _command.Examples.Add(args); } - public void SetDefaultCommand() + public void SetDefaultCommand<[DynamicallyAccessedMembers(PublicConstructors | Interfaces)] TDefaultCommand>() where TDefaultCommand : class, ICommandLimiter { var defaultCommand = ConfiguredCommand.FromType( @@ -36,7 +36,7 @@ public void HideBranch() _command.IsHidden = true; } - public ICommandConfigurator AddCommand(string name) + public ICommandConfigurator AddCommand<[DynamicallyAccessedMembers(PublicConstructors | Interfaces)] TCommand>(string name) where TCommand : class, ICommandLimiter { var command = ConfiguredCommand.FromType(name, isDefaultCommand: false); @@ -75,7 +75,7 @@ public IBranchConfigurator AddBranch(string name, Action action) { var command = ConfiguredCommand.FromBranch(settings, name); diff --git a/src/Spectre.Console.Cli/Internal/Configuration/ConfiguredCommand.cs b/src/Spectre.Console.Cli/Internal/Configuration/ConfiguredCommand.cs index e39663f1f..527ac2a78 100644 --- a/src/Spectre.Console.Cli/Internal/Configuration/ConfiguredCommand.cs +++ b/src/Spectre.Console.Cli/Internal/Configuration/ConfiguredCommand.cs @@ -7,7 +7,13 @@ internal sealed class ConfiguredCommand public string? Description { get; set; } public object? Data { get; set; } public Type? CommandType { get; } - public Type SettingsType { get; } + + public Type SettingsType + { + [return: DynamicallyAccessedMembers(PublicConstructors | PublicProperties)] + get; + } + public Func>? Delegate { get; } public bool IsDefaultCommand { get; } public bool IsHidden { get; set; } @@ -47,7 +53,7 @@ public static ConfiguredCommand FromBranch(string name) return new ConfiguredCommand(name, null, typeof(TSettings), null, false); } - public static ConfiguredCommand FromType(string name, bool isDefaultCommand = false) + public static ConfiguredCommand FromType<[DynamicallyAccessedMembers(Interfaces)] TCommand>(string name, bool isDefaultCommand = false) where TCommand : class, ICommand { var settingsType = ConfigurationHelper.GetSettingsType(typeof(TCommand)); diff --git a/src/Spectre.Console.Cli/Internal/DefaultPairDeconstructor.cs b/src/Spectre.Console.Cli/Internal/DefaultPairDeconstructor.cs index 8d1e73ebe..72fbeaa1b 100644 --- a/src/Spectre.Console.Cli/Internal/DefaultPairDeconstructor.cs +++ b/src/Spectre.Console.Cli/Internal/DefaultPairDeconstructor.cs @@ -28,9 +28,7 @@ internal sealed class DefaultPairDeconstructor : IPairDeconstructor // Got a default constructor? if (valueType.IsValueType) { - // Get the string variant of a default instance. - // Should not get null here, but compiler doesn't know that. - stringValue = Activator.CreateInstance(valueType)?.ToString() ?? string.Empty; + stringValue = TypeConverterHelper.GetDefaultValueOfType(valueType); } else { @@ -48,7 +46,7 @@ internal sealed class DefaultPairDeconstructor : IPairDeconstructor { try { - var converter = GetConverter(targetType); + var converter = TypeConverterHelper.GetTypeConverter(targetType); return converter.ConvertFrom(value); } catch @@ -57,15 +55,4 @@ internal sealed class DefaultPairDeconstructor : IPairDeconstructor throw CommandParseException.ValueIsNotInValidFormat(value); } } - - private static TypeConverter GetConverter(Type type) - { - var converter = TypeDescriptor.GetConverter(type); - if (converter != null) - { - return converter; - } - - throw new CommandConfigurationException($"Could find a type converter for '{type.FullName}'."); - } } \ No newline at end of file diff --git a/src/Spectre.Console.Cli/Internal/Modelling/CommandInfo.cs b/src/Spectre.Console.Cli/Internal/Modelling/CommandInfo.cs index 08bad48dd..254e16df9 100644 --- a/src/Spectre.Console.Cli/Internal/Modelling/CommandInfo.cs +++ b/src/Spectre.Console.Cli/Internal/Modelling/CommandInfo.cs @@ -6,8 +6,19 @@ internal sealed class CommandInfo : ICommandContainer, ICommandInfo public HashSet Aliases { get; } public string? Description { get; } public object? Data { get; } - public Type? CommandType { get; } - public Type SettingsType { get; } + + public Type? CommandType + { + [return: DynamicallyAccessedMembers(PublicConstructors | PublicProperties)] + get; + } + + public Type SettingsType + { + [return: DynamicallyAccessedMembers(PublicConstructors | PublicProperties)] + get; + } + public Func>? Delegate { get; } public bool IsDefaultCommand { get; } public CommandInfo? Parent { get; } diff --git a/src/Spectre.Console.Cli/Internal/Modelling/CommandModel.cs b/src/Spectre.Console.Cli/Internal/Modelling/CommandModel.cs index 721960bd3..7d7c0577c 100644 --- a/src/Spectre.Console.Cli/Internal/Modelling/CommandModel.cs +++ b/src/Spectre.Console.Cli/Internal/Modelling/CommandModel.cs @@ -47,7 +47,7 @@ private static string GetApplicationName(string? applicationName) private static string? GetApplicationFile() { - var location = Assembly.GetEntryAssembly()?.Location; + var location = AppContext.BaseDirectory; if (string.IsNullOrWhiteSpace(location)) { diff --git a/src/Spectre.Console.Cli/Internal/Modelling/CommandParameter.cs b/src/Spectre.Console.Cli/Internal/Modelling/CommandParameter.cs index e461d50c0..145855a04 100644 --- a/src/Spectre.Console.Cli/Internal/Modelling/CommandParameter.cs +++ b/src/Spectre.Console.Cli/Internal/Modelling/CommandParameter.cs @@ -3,7 +3,13 @@ namespace Spectre.Console.Cli; internal abstract class CommandParameter : ICommandParameterInfo, ICommandParameter { public Guid Id { get; } - public Type ParameterType { get; } + + public Type ParameterType + { + [return: DynamicallyAccessedMembers(PublicConstructors | Interfaces)] + get; + } + public ParameterKind ParameterKind { get; } public PropertyInfo Property { get; } public string? Description { get; } @@ -64,7 +70,7 @@ public void Assign(CommandSettings settings, ITypeResolver resolver, object? val var multimap = (IMultiMap?)Property.GetValue(settings); if (multimap == null) { - multimap = Activator.CreateInstance(typeof(MultiMap<,>).MakeGenericType(genericTypes[0], genericTypes[1])) as IMultiMap; + multimap = CreateInstanceHelpers.CreateMultiMapInstance(genericTypes[0], genericTypes[1]); if (multimap == null) { throw new InvalidOperationException("Could not create multimap"); @@ -98,19 +104,13 @@ public void Assign(CommandSettings settings, ITypeResolver resolver, object? val var array = (Array?)Property.GetValue(settings); Array newArray; - var elementType = Property.PropertyType.GetElementType(); - if (elementType == null) - { - throw new InvalidOperationException("Could not get property type."); - } - if (array == null) { - newArray = Array.CreateInstance(elementType, 1); + newArray = CreateInstanceHelpers.CreateArrayInstance(Property.PropertyType, 1); } else { - newArray = Array.CreateInstance(elementType, array.Length + 1); + newArray = CreateInstanceHelpers.CreateArrayInstance(Property.PropertyType, array.Length + 1); array.CopyTo(newArray, 0); } diff --git a/src/Spectre.Console.Cli/Internal/Modelling/CommandParameterComparer.cs b/src/Spectre.Console.Cli/Internal/Modelling/CommandParameterComparer.cs index 6997b8bfa..9f2b24463 100644 --- a/src/Spectre.Console.Cli/Internal/Modelling/CommandParameterComparer.cs +++ b/src/Spectre.Console.Cli/Internal/Modelling/CommandParameterComparer.cs @@ -18,12 +18,12 @@ public bool Equals(CommandParameter? x, CommandParameter? y) return true; } - return x.Property.MetadataToken == y.Property.MetadataToken; + return x.Property.Equals(y.Property); } public int GetHashCode(CommandParameter? obj) { - return obj?.Property?.MetadataToken.GetHashCode() ?? 0; + return obj?.Property?.GetHashCode() ?? 0; } } } \ No newline at end of file diff --git a/src/Spectre.Console.Cli/Internal/TypeRegistrar.cs b/src/Spectre.Console.Cli/Internal/TypeRegistrar.cs index 1fc0445cc..0b11000a6 100644 --- a/src/Spectre.Console.Cli/Internal/TypeRegistrar.cs +++ b/src/Spectre.Console.Cli/Internal/TypeRegistrar.cs @@ -9,7 +9,7 @@ internal TypeRegistrar(ITypeRegistrar registrar) _registrar = registrar ?? throw new ArgumentNullException(nameof(registrar)); } - public void Register() + public void Register() where TImplementation : TService { _registrar.Register(typeof(TService), typeof(TImplementation)); diff --git a/src/Spectre.Console.Cli/Internal/TypeResolverAdapter.cs b/src/Spectre.Console.Cli/Internal/TypeResolverAdapter.cs index c2cc3603c..9afceafb7 100644 --- a/src/Spectre.Console.Cli/Internal/TypeResolverAdapter.cs +++ b/src/Spectre.Console.Cli/Internal/TypeResolverAdapter.cs @@ -25,7 +25,12 @@ public TypeResolverAdapter(ITypeResolver? resolver) } // Fall back to use the activator. - return Activator.CreateInstance(type); + if (CreateInstanceHelpers.TryGetInstance(type, [], out var instance)) + { + return instance; + } + + throw CommandRuntimeException.CouldNotResolveType(type); } catch (CommandAppException) { diff --git a/src/Spectre.Console.Cli/PairDeconstructor.cs b/src/Spectre.Console.Cli/PairDeconstructor.cs index 318c32829..15cdd69bd 100644 --- a/src/Spectre.Console.Cli/PairDeconstructor.cs +++ b/src/Spectre.Console.Cli/PairDeconstructor.cs @@ -5,7 +5,7 @@ namespace Spectre.Console.Cli; /// /// The key type. /// The value type. -public abstract class PairDeconstructor : IPairDeconstructor +public abstract class PairDeconstructor<[DynamicallyAccessedMembers(All)] TKey, [DynamicallyAccessedMembers(All)] TValue> : IPairDeconstructor { /// /// Deconstructs the provided into a pair. @@ -33,6 +33,6 @@ public abstract class PairDeconstructor : IPairDeconstructor /// The value type. /// This class is misspelled, use instead. [Obsolete("Use PairDeconstructor instead")] -public abstract class PairDeconstuctor : PairDeconstructor +public abstract class PairDeconstuctor<[DynamicallyAccessedMembers(All)] TKey, [DynamicallyAccessedMembers(All)] TValue> : PairDeconstructor { } \ No newline at end of file diff --git a/src/Spectre.Console.Cli/Properties/Usings.cs b/src/Spectre.Console.Cli/Properties/Usings.cs index 48d442f5e..9f381a2df 100644 --- a/src/Spectre.Console.Cli/Properties/Usings.cs +++ b/src/Spectre.Console.Cli/Properties/Usings.cs @@ -13,4 +13,5 @@ global using System.Xml; global using Spectre.Console.Cli.Help; global using Spectre.Console.Cli.Unsafe; -global using Spectre.Console.Rendering; \ No newline at end of file +global using Spectre.Console.Rendering; +global using static System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes; \ No newline at end of file diff --git a/src/Spectre.Console.Cli/Spectre.Console.Cli.csproj b/src/Spectre.Console.Cli/Spectre.Console.Cli.csproj index 17c2a24b2..d76bace0d 100644 --- a/src/Spectre.Console.Cli/Spectre.Console.Cli.csproj +++ b/src/Spectre.Console.Cli/Spectre.Console.Cli.csproj @@ -5,11 +5,12 @@ true - false - false + true + true + diff --git a/src/Spectre.Console.Cli/Unsafe/IUnsafeConfigurator.cs b/src/Spectre.Console.Cli/Unsafe/IUnsafeConfigurator.cs index dd8904372..ee022569f 100644 --- a/src/Spectre.Console.Cli/Unsafe/IUnsafeConfigurator.cs +++ b/src/Spectre.Console.Cli/Unsafe/IUnsafeConfigurator.cs @@ -11,7 +11,7 @@ public interface IUnsafeConfigurator /// The name of the command. /// The command type. /// A command configurator that can be used to configure the command further. - ICommandConfigurator AddCommand(string name, Type command); + ICommandConfigurator AddCommand(string name, [DynamicallyAccessedMembers(PublicConstructors | Interfaces)] Type command); /// /// Adds a command branch. @@ -20,5 +20,6 @@ public interface IUnsafeConfigurator /// The command setting type. /// The command branch configurator. /// A branch configurator that can be used to configure the branch further. + [RequiresDynamicCode("Calls System.Type.MakeGenericType(params Type[])")] IBranchConfigurator AddBranch(string name, Type settings, Action action); } \ No newline at end of file diff --git a/src/Spectre.Console.Testing/Spectre.Console.Testing.csproj b/src/Spectre.Console.Testing/Spectre.Console.Testing.csproj index 182a63d02..35a20ad0f 100644 --- a/src/Spectre.Console.Testing/Spectre.Console.Testing.csproj +++ b/src/Spectre.Console.Testing/Spectre.Console.Testing.csproj @@ -10,6 +10,7 @@ + diff --git a/src/Spectre.Console.sln b/src/Spectre.Console.sln index 756d7c138..2d100d26a 100644 --- a/src/Spectre.Console.sln +++ b/src/Spectre.Console.sln @@ -42,6 +42,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{F34EFD87 Tests\.editorconfig = Tests\.editorconfig EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Spectre.Console.TrimTest", "Tests\Spectre.Console.TrimTest\Spectre.Console.TrimTest.csproj", "{E4B3EC1E-C6CE-49BE-AF46-15C1E22001E0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -136,6 +138,18 @@ Global {579E6E31-1E2F-4FE1-8F8C-9770878993E9}.Release|x64.Build.0 = Release|Any CPU {579E6E31-1E2F-4FE1-8F8C-9770878993E9}.Release|x86.ActiveCfg = Release|Any CPU {579E6E31-1E2F-4FE1-8F8C-9770878993E9}.Release|x86.Build.0 = Release|Any CPU + {E4B3EC1E-C6CE-49BE-AF46-15C1E22001E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E4B3EC1E-C6CE-49BE-AF46-15C1E22001E0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E4B3EC1E-C6CE-49BE-AF46-15C1E22001E0}.Debug|x64.ActiveCfg = Debug|Any CPU + {E4B3EC1E-C6CE-49BE-AF46-15C1E22001E0}.Debug|x64.Build.0 = Debug|Any CPU + {E4B3EC1E-C6CE-49BE-AF46-15C1E22001E0}.Debug|x86.ActiveCfg = Debug|Any CPU + {E4B3EC1E-C6CE-49BE-AF46-15C1E22001E0}.Debug|x86.Build.0 = Debug|Any CPU + {E4B3EC1E-C6CE-49BE-AF46-15C1E22001E0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E4B3EC1E-C6CE-49BE-AF46-15C1E22001E0}.Release|Any CPU.Build.0 = Release|Any CPU + {E4B3EC1E-C6CE-49BE-AF46-15C1E22001E0}.Release|x64.ActiveCfg = Release|Any CPU + {E4B3EC1E-C6CE-49BE-AF46-15C1E22001E0}.Release|x64.Build.0 = Release|Any CPU + {E4B3EC1E-C6CE-49BE-AF46-15C1E22001E0}.Release|x86.ActiveCfg = Release|Any CPU + {E4B3EC1E-C6CE-49BE-AF46-15C1E22001E0}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -146,6 +160,7 @@ Global {579E6E31-1E2F-4FE1-8F8C-9770878993E9} = {E0E45070-123C-4A4D-AA98-2A780308876C} {60A4CADD-2B3D-48ED-89C0-1637A1F111AE} = {F34EFD87-6CEA-453F-858B-094EA413578C} {E07C46D2-714F-4116-BADD-FEE09617A9C4} = {F34EFD87-6CEA-453F-858B-094EA413578C} + {E4B3EC1E-C6CE-49BE-AF46-15C1E22001E0} = {F34EFD87-6CEA-453F-858B-094EA413578C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5729B071-67A0-48FB-8B1B-275E6822086C} diff --git a/src/Spectre.Console/Internal/TypeConverterHelper.cs b/src/Spectre.Console/Internal/TypeConverterHelper.cs index da08d2b77..bedac91ad 100644 --- a/src/Spectre.Console/Internal/TypeConverterHelper.cs +++ b/src/Spectre.Console/Internal/TypeConverterHelper.cs @@ -2,7 +2,7 @@ namespace Spectre.Console; internal static class TypeConverterHelper { - internal const DynamicallyAccessedMemberTypes ConverterAnnotation = DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields; + internal const DynamicallyAccessedMemberTypes ConverterAnnotation = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicFields; internal static bool IsGetConverterSupported => !AppContext.TryGetSwitch("Spectre.Console.TypeConverterHelper.IsGetConverterSupported ", out var enabled) || enabled; @@ -96,9 +96,7 @@ public static TypeConverter GetTypeConverter() } } - private delegate TypeConverter FuncWithDam([DynamicallyAccessedMembers(ConverterAnnotation)] Type type); - - private static readonly Dictionary _intrinsicConverters; + private static readonly Dictionary> _intrinsicConverters; static TypeConverterHelper() { @@ -140,7 +138,7 @@ static TypeConverterHelper() } [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2111", Justification = "Delegate reflection is safe for all usages in this type.")] - private static FuncWithDam CreateEnumConverter() => ([DynamicallyAccessedMembers(ConverterAnnotation)] Type type) => new EnumConverter(type); + private static Func CreateEnumConverter() => ([DynamicallyAccessedMembers(ConverterAnnotation)] Type type) => new EnumConverter(type); /// /// A highly-constrained version of that only returns intrinsic converters. diff --git a/src/Tests/Spectre.Console.TrimTest/Commands/Add/AddPackageCommand.cs b/src/Tests/Spectre.Console.TrimTest/Commands/Add/AddPackageCommand.cs new file mode 100644 index 000000000..48c354b14 --- /dev/null +++ b/src/Tests/Spectre.Console.TrimTest/Commands/Add/AddPackageCommand.cs @@ -0,0 +1,46 @@ +using System.ComponentModel; +using Spectre.Console.Cli; +using Spectre.Console.TrimTest.Utilities; + +namespace Spectre.Console.TrimTest.Commands.Add; + +[Description("Add a NuGet package reference to the project.")] +public sealed class AddPackageCommand : Command +{ + public sealed class Settings : AddSettings + { + [CommandArgument(0, "")] + [Description("The package reference to add.")] + public string PackageName { get; set; } + + [CommandOption("-v|--version ")] + [Description("The version of the package to add.")] + public string Version { get; set; } + + [CommandOption("-f|--framework ")] + [Description("Add the reference only when targeting a specific framework.")] + public string Framework { get; set; } + + [CommandOption("--no-restore")] + [Description("Add the reference without performing restore preview and compatibility check.")] + public bool NoRestore { get; set; } + + [CommandOption("--source ")] + [Description("The NuGet package source to use during the restore.")] + public string Source { get; set; } + + [CommandOption("--package-directory ")] + [Description("The directory to restore packages to.")] + public string PackageDirectory { get; set; } + + [CommandOption("--interactive")] + [Description("Allows the command to stop and wait for user input or action (for example to complete authentication).")] + public bool Interactive { get; set; } + } + + public override int Execute(CommandContext context, Settings settings) + { + SettingsDumper.Dump(settings); + return 0; + } +} diff --git a/src/Tests/Spectre.Console.TrimTest/Commands/Add/AddReferenceCommand.cs b/src/Tests/Spectre.Console.TrimTest/Commands/Add/AddReferenceCommand.cs new file mode 100644 index 000000000..5627f036e --- /dev/null +++ b/src/Tests/Spectre.Console.TrimTest/Commands/Add/AddReferenceCommand.cs @@ -0,0 +1,29 @@ +using System.ComponentModel; +using Spectre.Console.Cli; +using Spectre.Console.TrimTest.Utilities; + +namespace Spectre.Console.TrimTest.Commands.Add; + +public sealed class AddReferenceCommand : Command +{ + public sealed class Settings : AddSettings + { + [CommandArgument(0, "")] + [Description("The package reference to add.")] + public string ProjectPath { get; set; } + + [CommandOption("-f|--framework ")] + [Description("Add the reference only when targeting a specific framework.")] + public string Framework { get; set; } + + [CommandOption("--interactive")] + [Description("Allows the command to stop and wait for user input or action (for example to complete authentication).")] + public bool Interactive { get; set; } + } + + public override int Execute(CommandContext context, Settings settings) + { + SettingsDumper.Dump(settings); + return 0; + } +} diff --git a/src/Tests/Spectre.Console.TrimTest/Commands/Add/AddSettings.cs b/src/Tests/Spectre.Console.TrimTest/Commands/Add/AddSettings.cs new file mode 100644 index 000000000..240987e00 --- /dev/null +++ b/src/Tests/Spectre.Console.TrimTest/Commands/Add/AddSettings.cs @@ -0,0 +1,11 @@ +using System.ComponentModel; +using Spectre.Console.Cli; + +namespace Spectre.Console.TrimTest.Commands.Add; + +public abstract class AddSettings : CommandSettings +{ + [CommandArgument(0, "")] + [Description("The project file to operate on. If a file is not specified, the command will search the current directory for one.")] + public string Project { get; set; } +} diff --git a/src/Tests/Spectre.Console.TrimTest/Commands/Run/RunCommand.cs b/src/Tests/Spectre.Console.TrimTest/Commands/Run/RunCommand.cs new file mode 100644 index 000000000..a809e1552 --- /dev/null +++ b/src/Tests/Spectre.Console.TrimTest/Commands/Run/RunCommand.cs @@ -0,0 +1,72 @@ +using System.ComponentModel; +using Spectre.Console.Cli; +using Spectre.Console.TrimTest.Utilities; + +namespace Spectre.Console.TrimTest.Commands.Run; + +[Description("Build and run a .NET project output.")] +public sealed class RunCommand : Command +{ + public sealed class Settings : CommandSettings + { + [CommandOption("-c|--configuration ")] + [Description("The configuration to run for. The default for most projects is '[grey]Debug[/]'.")] + [DefaultValue("Debug")] + public string Configuration { get; set; } + + [CommandOption("-f|--framework ")] + [Description("The target framework to run for. The target framework must also be specified in the project file.")] + public string Framework { get; set; } + + [CommandOption("-r|--runtime ")] + [Description("The target runtime to run for.")] + public string RuntimeIdentifier { get; set; } + + [CommandOption("-p|--project ", typeof(DirectoryInfo))] + [Description("The path to the project file to run (defaults to the current directory if there is only one project).")] + public DirectoryInfo ProjectPath { get; set; } + + [CommandOption("--launch-profile ")] + [Description("The name of the launch profile (if any) to use when launching the application.")] + public string LaunchProfile { get; set; } + + [CommandOption("--no-launch-profile")] + [Description("Do not attempt to use [grey]launchSettings.json[/] to configure the application.")] + public bool NoLaunchProfile { get; set; } + + [CommandOption("--no-build")] + [Description("Do not build the project before running. Implies [grey]--no-restore[/].")] + public bool NoBuild { get; set; } + + [CommandOption("--interactive")] + [Description("Allows the command to stop and wait for user input or action (for example to complete authentication).")] + public string Interactive { get; set; } + + [CommandOption("--no-restore")] + [Description("Do not restore the project before building.")] + public bool NoRestore { get; set; } + + [CommandOption("--verbosity ")] + [Description("Set the MSBuild verbosity level. Allowed values are q[grey]uiet[/], m[grey]inimal[/], n[grey]ormal[/], d[grey]etailed[/], and diag[grey]nostic[/].")] + [TypeConverter(typeof(VerbosityConverter))] + [DefaultValue(Verbosity.Normal)] + public Verbosity Verbosity { get; set; } + + [CommandOption("--no-dependencies")] + [Description("Do not restore project-to-project references and only restore the specified project.")] + public bool NoDependencies { get; set; } + + [CommandOption("--force")] + [Description("Force all dependencies to be resolved even if the last restore was successful. This is equivalent to deleting [grey]project.assets.json[/].")] + public bool Force { get; set; } + + [CommandOption("--var ")] + public IDictionary Values { get; set; } + } + + public override int Execute(CommandContext context, Settings settings) + { + SettingsDumper.Dump(settings); + return 0; + } +} diff --git a/src/Tests/Spectre.Console.TrimTest/Commands/Serve/ServeCommand.cs b/src/Tests/Spectre.Console.TrimTest/Commands/Serve/ServeCommand.cs new file mode 100644 index 000000000..4404d3645 --- /dev/null +++ b/src/Tests/Spectre.Console.TrimTest/Commands/Serve/ServeCommand.cs @@ -0,0 +1,39 @@ +using System.ComponentModel; +using Spectre.Console.Cli; +using Spectre.Console.TrimTest.Utilities; + +namespace Spectre.Console.TrimTest.Commands.Serve; + +[Description("Launches a web server in the current working directory and serves all files in it.")] +public sealed class ServeCommand : Command +{ + public sealed class Settings : CommandSettings + { + [CommandOption("-p|--port ")] + [Description("Port to use. Defaults to [grey]8080[/]. Use [grey]0[/] for a dynamic port.")] + public int Port { get; set; } + + [CommandOption("-o|--open-browser [BROWSER]")] + [Description("Open a web browser when the server starts. You can also specify which browser to use. If none is specified, the default one will be used.")] + public FlagValue OpenBrowser { get; set; } + } + + public override int Execute(CommandContext context, Settings settings) + { + if (settings.OpenBrowser.IsSet) + { + var browser = settings.OpenBrowser.Value; + if (browser != null) + { + System.Console.WriteLine($"Open in {browser}"); + } + else + { + System.Console.WriteLine($"Open in default browser."); + } + } + + SettingsDumper.Dump(settings); + return 0; + } +} diff --git a/src/Tests/Spectre.Console.TrimTest/MyInterceptor.cs b/src/Tests/Spectre.Console.TrimTest/MyInterceptor.cs new file mode 100644 index 000000000..9af5ac30e --- /dev/null +++ b/src/Tests/Spectre.Console.TrimTest/MyInterceptor.cs @@ -0,0 +1,20 @@ +using Spectre.Console.Cli; +using Spectre.Console.TrimTest.Commands.Run; + +namespace Spectre.Console.TrimTest; + +internal class MyInterceptor : ICommandInterceptor +{ + public void Intercept(CommandContext context, CommandSettings settings) + { + if (settings is not RunCommand.Settings runCommandSettings) + { + return; + } + + if (runCommandSettings.Framework.Length > 0 && !runCommandSettings.Framework.StartsWith('v')) + { + runCommandSettings.Framework = "v" + runCommandSettings.Framework; + } + } +} \ No newline at end of file diff --git a/src/Tests/Spectre.Console.TrimTest/Program.cs b/src/Tests/Spectre.Console.TrimTest/Program.cs new file mode 100644 index 000000000..77e82b446 --- /dev/null +++ b/src/Tests/Spectre.Console.TrimTest/Program.cs @@ -0,0 +1,33 @@ +using Spectre.Console.Cli; +using Spectre.Console.TrimTest; +using Spectre.Console.TrimTest.Commands.Add; +using Spectre.Console.TrimTest.Commands.Run; +using Spectre.Console.TrimTest.Commands.Serve; + +var app = new CommandApp(); +app.Configure(config => +{ + config.PropagateExceptions(); + config.SetApplicationName("fake-dotnet"); + config.ValidateExamples(); + config.SetInterceptor(new MyInterceptor()); + config.AddExample("run", "--no-build"); + + // Run + config.AddCommand("run"); + + // Add + config.AddBranch("add", add => + { + add.SetDescription("Add a package or reference to a .NET project"); + add.AddCommand("package"); + add.AddCommand("reference"); + }); + + // Serve + config.AddCommand("serve") + .WithExample("serve", "-o", "firefox") + .WithExample("serve", "--port", "80", "-o", "firefox"); +}); + +return app.Run(args); \ No newline at end of file diff --git a/src/Tests/Spectre.Console.TrimTest/Spectre.Console.TrimTest.csproj b/src/Tests/Spectre.Console.TrimTest/Spectre.Console.TrimTest.csproj new file mode 100644 index 000000000..24cb466ee --- /dev/null +++ b/src/Tests/Spectre.Console.TrimTest/Spectre.Console.TrimTest.csproj @@ -0,0 +1,22 @@ + + + + Exe + net9.0 + enable + enable + true + CS8618;IL2078 + + + + + + + + + + + + + diff --git a/src/Tests/Spectre.Console.TrimTest/Utilities/SettingsDumper.cs b/src/Tests/Spectre.Console.TrimTest/Utilities/SettingsDumper.cs new file mode 100644 index 000000000..b6d9ca49d --- /dev/null +++ b/src/Tests/Spectre.Console.TrimTest/Utilities/SettingsDumper.cs @@ -0,0 +1,27 @@ +using System.Diagnostics.CodeAnalysis; +using Spectre.Console.Cli; + +namespace Spectre.Console.TrimTest.Utilities; + +public static class SettingsDumper +{ + public static void Dump<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(CommandSettings settings) + { + var table = new Table().RoundedBorder(); + table.AddColumn("[grey]Name[/]"); + table.AddColumn("[grey]Value[/]"); + + var properties = typeof(T).GetProperties(); + foreach (var property in properties) + { + var o = property.GetValue(settings); + var value = o?.ToString().EscapeMarkup(); + + table.AddRow( + property.Name, + value ?? "[grey]null[/]"); + } + + AnsiConsole.Write(table); + } +} diff --git a/src/Tests/Spectre.Console.TrimTest/Verbosity.cs b/src/Tests/Spectre.Console.TrimTest/Verbosity.cs new file mode 100644 index 000000000..549d20a69 --- /dev/null +++ b/src/Tests/Spectre.Console.TrimTest/Verbosity.cs @@ -0,0 +1,51 @@ +using System.ComponentModel; +using System.Globalization; + +namespace Spectre.Console.TrimTest; + +public enum Verbosity +{ + Quiet, + Minimal, + Normal, + Detailed, + Diagnostic +} + +public sealed class VerbosityConverter : TypeConverter +{ + private readonly Dictionary _lookup; + + public VerbosityConverter() + { + _lookup = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "q", Verbosity.Quiet }, + { "quiet", Verbosity.Quiet }, + { "m", Verbosity.Minimal }, + { "minimal", Verbosity.Minimal }, + { "n", Verbosity.Normal }, + { "normal", Verbosity.Normal }, + { "d", Verbosity.Detailed }, + { "detailed", Verbosity.Detailed }, + { "diag", Verbosity.Diagnostic }, + { "diagnostic", Verbosity.Diagnostic } + }; + } + + public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + if (value is string stringValue) + { + var result = _lookup.TryGetValue(stringValue, out var verbosity); + if (!result) + { + const string format = "The value '{0}' is not a valid verbosity."; + var message = string.Format(CultureInfo.InvariantCulture, format, value); + throw new InvalidOperationException(message); + } + return verbosity; + } + throw new NotSupportedException("Can't convert value to verbosity."); + } +}