Skip to content

Commit

Permalink
Marking UnsafeConfig unsafe for AOT, and adding more robust TypeConve…
Browse files Browse the repository at this point in the history
…rterHelper, testing DI
  • Loading branch information
phil-scott-78 committed Apr 18, 2024
1 parent db3ce54 commit 602f7de
Show file tree
Hide file tree
Showing 8 changed files with 221 additions and 22 deletions.
4 changes: 4 additions & 0 deletions examples/Cli/DemoAot/DemoAot.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Spectre.Console.Cli\Spectre.Console.Cli.csproj" />
<TrimmerRootAssembly Include="Spectre.Console" />
<TrimmerRootAssembly Include="Spectre.Console.Cli" />

<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
</ItemGroup>

</Project>
27 changes: 27 additions & 0 deletions examples/Cli/DemoAot/GreetingService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Spectre.Console;
using Spectre.Console.Cli;

namespace DemoAot;

public class InfoCommand(GreetingService greetingService) : Command<InfoCommand.Settings>
{
public override int Execute(CommandContext context, Settings settings)
{
greetingService.Greet("World");
return 0;
}

public class Settings : CommandSettings
{
[CommandOption("-v")]
public bool Verbose {get;set;}
}
}

public class GreetingService(IAnsiConsole ansiConsole)
{
public void Greet(string name)
{
ansiConsole.WriteLine($"Hello {name}!");
}
}
12 changes: 11 additions & 1 deletion examples/Cli/DemoAot/Program.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
using System;
using DemoAot;
using DemoAot.Commands.Add;
using DemoAot.Commands.Run;
using DemoAot.Commands.Serve;
using DemoAot.Utilities;
using Microsoft.Extensions.DependencyInjection;
using Spectre.Console;
using Spectre.Console.Cli;

try
{
var app = new CommandApp();
var services = new ServiceCollection();
services.AddSingleton<GreetingService>();

// add extra services to the container here
using var registrar = new DependencyInjectionRegistrar(services);

var app = new CommandApp(registrar);
app.Configure(config =>
{
config.PropagateExceptions();
Expand All @@ -17,6 +26,7 @@
// Run
config.AddCommand<RunCommand, RunCommand.Settings>("run");
config.AddCommand<InfoCommand, InfoCommand.Settings>("info");
// Add
config.AddBranch<AddSettings>("add", add =>
Expand Down
53 changes: 53 additions & 0 deletions examples/Cli/DemoAot/Utilities/DependencyInjectionRegistrar.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.DependencyInjection;
using Spectre.Console.Cli;

namespace DemoAot.Utilities;

public class DependencyInjectionRegistrar(IServiceCollection services) : ITypeRegistrar, IDisposable
{
private IServiceCollection Services { get; } = services;
private List<IDisposable> BuiltProviders { get; } = [];

public ITypeResolver Build()
{
var buildServiceProvider = Services.BuildServiceProvider();
BuiltProviders.Add(buildServiceProvider);
return new DependencyInjectionResolver(buildServiceProvider);
}

public void Register(Type service, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type implementation)
{
Services.AddSingleton(service, implementation);
}

public void RegisterInstance(Type service, object implementation)
{
Services.AddSingleton(service, implementation);
}

public void RegisterLazy(Type service, Func<object> factory)
{
Services.AddSingleton(service, _ => factory());
}

public void Dispose()
{
foreach (var provider in BuiltProviders)
{
provider.Dispose();
}
}
}

internal class DependencyInjectionResolver(ServiceProvider serviceProvider) : ITypeResolver, IDisposable
{
public void Dispose() => serviceProvider.Dispose();

public object Resolve(Type type)
{
return serviceProvider.GetService(type);
}
}
13 changes: 8 additions & 5 deletions src/Spectre.Console.Cli/Internal/Configuration/Configurator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,11 @@ public IBranchConfigurator AddBranch<
return new BranchConfigurator(added);
}

[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2060", Justification = TrimWarnings.SuppressMessage)]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050", Justification = TrimWarnings.SuppressMessage)]
ICommandConfigurator IUnsafeConfigurator.AddCommand(string name, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type command)
[RequiresDynamicCode("Uses MakeGenericType")]
[RequiresUnreferencedCode("If some of the generic arguments are annotated (either with DynamicallyAccessedMembersAttribute, or generic constraints), trimming can\'t validate that the requirements of those annotations are met.")]
ICommandConfigurator IUnsafeConfigurator.AddCommand(
string name,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.Interfaces)] Type command)
{
var method = GetType().GetMethods().FirstOrDefault(i => i.Name == "AddCommand" && i.GetGenericArguments().Length == 1);
if (method == null)
Expand All @@ -134,10 +136,11 @@ ICommandConfigurator IUnsafeConfigurator.AddCommand(string name, [DynamicallyAcc
return result;
}

[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050", Justification = TrimWarnings.SuppressMessage)]
[RequiresDynamicCode("Uses MakeGenericType")]
IBranchConfigurator IUnsafeConfigurator.AddBranch(
string name,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties)] Type settings,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties
| DynamicallyAccessedMemberTypes.Interfaces)]Type settings,
Action<IUnsafeBranchConfigurator> action)
{
var command = ConfiguredCommand.FromBranch(settings, name);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,11 @@ public IBranchConfigurator AddBranch<
return new BranchConfigurator(added);
}

[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2060", Justification = TrimWarnings.SuppressMessage)]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050", Justification = TrimWarnings.SuppressMessage)]
ICommandConfigurator IUnsafeConfigurator.AddCommand(string name, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type command)
[RequiresDynamicCode("Uses MakeGenericType")]
[RequiresUnreferencedCode("If some of the generic arguments are annotated (either with DynamicallyAccessedMembersAttribute, or generic constraints), trimming can\'t validate that the requirements of those annotations are met.")]
ICommandConfigurator IUnsafeConfigurator.AddCommand(string name, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors
| DynamicallyAccessedMemberTypes.PublicProperties
| DynamicallyAccessedMemberTypes.Interfaces)] Type command)
{
var method = GetType().GetMethods().FirstOrDefault(i => i.Name == "AddCommand" && i.GetGenericArguments().Length == 1);
if (method == null)
Expand All @@ -129,10 +131,12 @@ ICommandConfigurator IUnsafeConfigurator.AddCommand(string name, [DynamicallyAcc
return result;
}

[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050", Justification = TrimWarnings.SuppressMessage)]
[RequiresDynamicCode("Uses MakeGenericType")]
IBranchConfigurator IUnsafeConfigurator.AddBranch(
string name,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties)] Type settings,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors
| DynamicallyAccessedMemberTypes.PublicProperties
| DynamicallyAccessedMemberTypes.Interfaces)] Type settings,
Action<IUnsafeBranchConfigurator> action)
{
var command = ConfiguredCommand.FromBranch(settings, name);
Expand Down
9 changes: 7 additions & 2 deletions src/Spectre.Console.Cli/Unsafe/IUnsafeConfigurator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ public interface IUnsafeConfigurator
/// <param name="name">The name of the command.</param>
/// <param name="command">The command type.</param>
/// <returns>A command configurator that can be used to configure the command further.</returns>
ICommandConfigurator AddCommand(string name, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type command);
[RequiresDynamicCode("Uses MakeGenericType")]
[RequiresUnreferencedCode("If some of the generic arguments are annotated (either with DynamicallyAccessedMembersAttribute, or generic constraints), trimming can\'t validate that the requirements of those annotations are met.")]
ICommandConfigurator AddCommand(string name, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties
| DynamicallyAccessedMemberTypes.Interfaces)] Type command);

/// <summary>
/// Adds a command branch.
Expand All @@ -20,5 +23,7 @@ public interface IUnsafeConfigurator
/// <param name="settings">The command setting type.</param>
/// <param name="action">The command branch configurator.</param>
/// <returns>A branch configurator that can be used to configure the branch further.</returns>
IBranchConfigurator AddBranch(string name, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties)] Type settings, Action<IUnsafeBranchConfigurator> action);
[RequiresDynamicCode("Uses MakeGenericType")]
IBranchConfigurator AddBranch(string name, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties
| DynamicallyAccessedMemberTypes.Interfaces)] Type settings, Action<IUnsafeBranchConfigurator> action);
}
111 changes: 102 additions & 9 deletions src/Spectre.Console/Internal/TypeConverterHelper.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
using DynamicMember = System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute;

namespace Spectre.Console;

internal static class TypeConverterHelper
{
private const string TypeConverterWarningsCanBeIgnored = "Type converter warnings can be ignored. Intrinsic types are always included.";
private const DynamicallyAccessedMemberTypes ConverterAnnotation = DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields;

private static bool IsGetConverterSupported =>
!AppContext.TryGetSwitch("Spectre.Console.TypeConverterHelper.IsGetConverterSupported ", out var enabled) || enabled;

public static string ConvertToString<T>(T input)
public static string ConvertToString<[DynamicMember(ConverterAnnotation)] T>(T input)
{
var result = GetTypeConverter<T>().ConvertToInvariantString(input);
if (result == null)
Expand All @@ -15,7 +20,7 @@ public static string ConvertToString<T>(T input)
return result;
}

public static bool TryConvertFromString<T>(string input, [MaybeNull] out T? result)
public static bool TryConvertFromString<[DynamicMember(ConverterAnnotation)] T>(string input, [MaybeNull] out T? result)
{
try
{
Expand All @@ -29,7 +34,7 @@ public static bool TryConvertFromString<T>(string input, [MaybeNull] out T? resu
}
}

public static bool TryConvertFromStringWithCulture<T>(string input, CultureInfo? info, [MaybeNull] out T? result)
public static bool TryConvertFromStringWithCulture<[DynamicMember(ConverterAnnotation)] T>(string input, CultureInfo? info, [MaybeNull] out T? result)
{
try
{
Expand All @@ -51,11 +56,9 @@ public static bool TryConvertFromStringWithCulture<T>(string input, CultureInfo?
}
}

[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026", Justification = TypeConverterWarningsCanBeIgnored)]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2087", Justification = TypeConverterWarningsCanBeIgnored)]
public static TypeConverter GetTypeConverter<T>()
public static TypeConverter GetTypeConverter<[DynamicMember(ConverterAnnotation)] T>()
{
var converter = TypeDescriptor.GetConverter(typeof(T));
var converter = GetConverter();
if (converter != null)
{
return converter;
Expand All @@ -76,5 +79,95 @@ public static TypeConverter GetTypeConverter<T>()
}

throw new InvalidOperationException("Could not find type converter");

[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2087", 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()
{
if (!IsGetConverterSupported)
{
return GetIntrinsicConverter(typeof(T));
}

return TypeDescriptor.GetConverter(typeof(T));
}
}

private delegate TypeConverter FuncWithDam([DynamicMember(ConverterAnnotation)] Type type);

private static readonly Dictionary<Type, FuncWithDam> _intrinsicConverters;

static TypeConverterHelper()
{
_intrinsicConverters = new Dictionary<Type, FuncWithDam>
{
[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 NET7_0_OR_GREATER
[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
};
}

[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2111", Justification = "Delegate reflection is safe for all usages in this type.")]
private static FuncWithDam CreateEnumConverter() => ([DynamicMember(ConverterAnnotation)] type) => new EnumConverter(type);

/// <summary>
/// A highly-constrained version of <see cref="TypeDescriptor.GetConverter(Type)" /> that only returns intrinsic converters.
/// </summary>
private static TypeConverter? GetIntrinsicConverter([DynamicMember(ConverterAnnotation)] Type type)
{
if (type.IsArray)
{
type = typeof(Array);
}

if (typeof(ICollection).IsAssignableFrom(type))
{
type = typeof(ICollection);
}

if (type.IsEnum)
{
type = typeof(Enum);
}

if (_intrinsicConverters.TryGetValue(type, out var factory))
{
return factory(type);
}

return null;
}
}
}

0 comments on commit 602f7de

Please sign in to comment.