diff --git a/docs/samples/samples.sln b/docs/samples/samples.sln index 60aedb76..51c08441 100644 --- a/docs/samples/samples.sln +++ b/docs/samples/samples.sln @@ -67,6 +67,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HelloWorldAttributes", "hel EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KeyedServices", "dependency-injection\keyed\KeyedServices.csproj", "{30674755-09B2-41DC-8AAD-31AC4541E562}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "trimming", "trimming", "{05379A49-3DC7-FF82-2FE9-08CAF8CE680F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrimmingSample", "trimming\TrimmingSample.csproj", "{8A57091B-7A35-4EA6-A863-AC2076188700}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -389,6 +393,18 @@ Global {30674755-09B2-41DC-8AAD-31AC4541E562}.Release|x64.Build.0 = Release|Any CPU {30674755-09B2-41DC-8AAD-31AC4541E562}.Release|x86.ActiveCfg = Release|Any CPU {30674755-09B2-41DC-8AAD-31AC4541E562}.Release|x86.Build.0 = Release|Any CPU + {8A57091B-7A35-4EA6-A863-AC2076188700}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A57091B-7A35-4EA6-A863-AC2076188700}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A57091B-7A35-4EA6-A863-AC2076188700}.Debug|x64.ActiveCfg = Debug|Any CPU + {8A57091B-7A35-4EA6-A863-AC2076188700}.Debug|x64.Build.0 = Debug|Any CPU + {8A57091B-7A35-4EA6-A863-AC2076188700}.Debug|x86.ActiveCfg = Debug|Any CPU + {8A57091B-7A35-4EA6-A863-AC2076188700}.Debug|x86.Build.0 = Debug|Any CPU + {8A57091B-7A35-4EA6-A863-AC2076188700}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A57091B-7A35-4EA6-A863-AC2076188700}.Release|Any CPU.Build.0 = Release|Any CPU + {8A57091B-7A35-4EA6-A863-AC2076188700}.Release|x64.ActiveCfg = Release|Any CPU + {8A57091B-7A35-4EA6-A863-AC2076188700}.Release|x64.Build.0 = Release|Any CPU + {8A57091B-7A35-4EA6-A863-AC2076188700}.Release|x86.ActiveCfg = Release|Any CPU + {8A57091B-7A35-4EA6-A863-AC2076188700}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -409,6 +425,7 @@ Global {A4F3D30A-4124-49B0-A888-F81CE682F491} = {13EC390A-3D6F-4A7E-A02C-D3CBA197FA40} {B38FF032-9AFB-481F-A432-DFA7A5100C3A} = {13EC390A-3D6F-4A7E-A02C-D3CBA197FA40} {30674755-09B2-41DC-8AAD-31AC4541E562} = {E03982B3-80F2-4D55-9501-378EACE93E5B} + {8A57091B-7A35-4EA6-A863-AC2076188700} = {05379A49-3DC7-FF82-2FE9-08CAF8CE680F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {43B81702-C1EF-43B6-B57D-C630AD1F555D} diff --git a/docs/samples/trimming/Program.cs b/docs/samples/trimming/Program.cs new file mode 100644 index 00000000..1aa1faa4 --- /dev/null +++ b/docs/samples/trimming/Program.cs @@ -0,0 +1,25 @@ +// Copyright (c) Nate McMaster. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using McMaster.Extensions.CommandLineUtils; + +// Reproduces https://github.com/natemcmaster/CommandLineUtils/issues/539: +// a trimmed, self-contained, single-file publish using CommandLineApplication.Execute +// with an attribute-based command model. Before the fix, a trimmed publish either warned +// during publish (when EnableTrimAnalyzer is set) or threw at run time with +// "No method named 'OnExecute' or 'OnExecuteAsync' could be found." / +// "Could not find any public constructors of type 'MyCommand'." +return CommandLineApplication.Execute(args); + +internal class MyCommand +{ + [Option(Description = "The subject to greet")] + public string Subject { get; } = "world"; + + private int OnExecute() + { + Console.WriteLine($"Hello, {Subject}!"); + return 0; + } +} diff --git a/docs/samples/trimming/README.md b/docs/samples/trimming/README.md new file mode 100644 index 00000000..be5dfcfa --- /dev/null +++ b/docs/samples/trimming/README.md @@ -0,0 +1,29 @@ +Trimming sample for McMaster.Extensions.CommandLineUtils +========================================================== + +Demonstrates publishing a trimmed, self-contained, single-file app that uses +`McMaster.Extensions.CommandLineUtils.CommandLineApplication.Execute`. + +See [issue #539](https://github.com/natemcmaster/CommandLineUtils/issues/539). + +Instructions +------------ + +From the repository root, publish the sample for your runtime (Linux x64 example): + +```bash +dotnet publish docs/samples/trimming/TrimmingSample.csproj -c Release -r linux-x64 +``` + +Because the project sets `EnableTrimAnalyzer` and `TreatWarningsAsErrors`, the publish +itself fails if the trimmer finds any unannotated reflection reachable from this sample - +so this project doubles as a regression check, not just a demo. + +Run the produced binary (path is printed at the end of the publish, under +`docs/samples/trimming/bin/Release/net8.0//publish`): + +```bash +./docs/samples/trimming/bin/Release/net8.0/linux-x64/publish/TrimmingSample --subject Trimming +``` + +Expected output: `Hello, Trimming!` diff --git a/docs/samples/trimming/TrimmingSample.csproj b/docs/samples/trimming/TrimmingSample.csproj new file mode 100644 index 00000000..6d89ce4a --- /dev/null +++ b/docs/samples/trimming/TrimmingSample.csproj @@ -0,0 +1,26 @@ + + + + Exe + net8.0 + enable + true + false + + + true + true + true + + + true + true + + + + + + + diff --git a/src/CommandLineUtils/Abstractions/IModelAccessor.cs b/src/CommandLineUtils/Abstractions/IModelAccessor.cs index 1dccb36c..6043a332 100644 --- a/src/CommandLineUtils/Abstractions/IModelAccessor.cs +++ b/src/CommandLineUtils/Abstractions/IModelAccessor.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Diagnostics.CodeAnalysis; namespace McMaster.Extensions.CommandLineUtils.Abstractions { @@ -14,6 +15,7 @@ public interface IModelAccessor /// Gets the type of the model. /// /// The type. + [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type GetModelType(); /// diff --git a/src/CommandLineUtils/Abstractions/ValueParserProvider.cs b/src/CommandLineUtils/Abstractions/ValueParserProvider.cs index 12e96000..55baa236 100644 --- a/src/CommandLineUtils/Abstractions/ValueParserProvider.cs +++ b/src/CommandLineUtils/Abstractions/ValueParserProvider.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Reflection; @@ -48,17 +49,29 @@ internal ValueParserProvider() /// public CultureInfo ParseCulture { get; set; } = CultureInfo.CurrentCulture; - private static readonly MethodInfo s_getParserGeneric - = typeof(ValueParserProvider) + private static readonly MethodInfo s_getParserGeneric; + + [UnconditionalSuppressMessage("Trimming", "IL2111", + Justification = "GetParser() is looked up here purely so GetParser(Type) can re-dispatch to it " + + "generically below; the annotations on its T parameter are honored because the Type passed " + + "into GetParser(Type) is itself annotated with DynamicallyAccessedMemberTypes.All.")] + static ValueParserProvider() + { + s_getParserGeneric = typeof(ValueParserProvider) .GetMethods(BindingFlags.Instance | BindingFlags.Public) .Single(m => m.Name == nameof(GetParser) && m.IsGenericMethod); + } /// /// Returns a parser registered for the given type. /// /// /// - public IValueParser GetParser(Type type) + [UnconditionalSuppressMessage("Trimming", "IL2060", + Justification = "type is annotated with DynamicallyAccessedMemberTypes.All, so the members " + + "GetParser() needs from it are preserved even though MakeGenericMethod's own use of type " + + "cannot be statically verified by the trimmer.")] + public IValueParser GetParser([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type type) { var method = s_getParserGeneric.MakeGenericMethod(type); #pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. @@ -76,7 +89,7 @@ public IValueParser GetParser(Type type) /// /// /// - public IValueParser? GetParser() + public IValueParser? GetParser<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>() { var parser = GetParserImpl(); return parser switch @@ -87,7 +100,17 @@ public IValueParser GetParser(Type type) }; } - internal IValueParser? GetParserImpl() + [UnconditionalSuppressMessage("Trimming", "IL2026", + Justification = "TryGetParser (TypeDescriptor-based) is only reached when T is not one of the " + + "stock value-parser types, an enum, or Nullable thereof. T is annotated with " + + "DynamicallyAccessedMemberTypes.All so its members survive trimming when this fallback runs.")] + [UnconditionalSuppressMessage("Trimming", "IL2072", + Justification = "type is typeof(T) where T is annotated with DynamicallyAccessedMemberTypes.All, so " + + "the array/wrapped element types reached through Type.GetElementType() and ReflectionHelper's " + + "Is*Type helpers are reachable through T even though those helpers do not themselves carry a " + + "matching annotation on their return/out value.")] + [UnconditionalSuppressMessage("Trimming", "IL2067", Justification = "See the IL2072 suppression above.")] + internal IValueParser? GetParserImpl<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>() { var type = typeof(T); if (_parsers.TryGetValue(type, out var parser)) diff --git a/src/CommandLineUtils/Attributes/OptionAttribute.cs b/src/CommandLineUtils/Attributes/OptionAttribute.cs index bf6b0b58..519af36e 100755 --- a/src/CommandLineUtils/Attributes/OptionAttribute.cs +++ b/src/CommandLineUtils/Attributes/OptionAttribute.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using McMaster.Extensions.CommandLineUtils.Abstractions; @@ -95,6 +96,11 @@ internal CommandOption Configure(CommandLineApplication app, PropertyInfo prop) return option; } + [UnconditionalSuppressMessage("Trimming", "IL2072", + Justification = "prop.PropertyType is not itself annotated (PropertyInfo.PropertyType has no " + + "DynamicallyAccessedMembers support), but prop's declaring model type flows from " + + "CommandLineApplication.Execute's DynamicallyAccessedMemberTypes.All-annotated TApp, so " + + "prop's own type is preserved by trimming.")] private CommandOptionType GetOptionType(PropertyInfo prop, ValueParserProvider valueParsers) { CommandOptionType optionType; diff --git a/src/CommandLineUtils/Attributes/VersionOptionFromMemberAttribute.cs b/src/CommandLineUtils/Attributes/VersionOptionFromMemberAttribute.cs index 058759df..48c5d322 100644 --- a/src/CommandLineUtils/Attributes/VersionOptionFromMemberAttribute.cs +++ b/src/CommandLineUtils/Attributes/VersionOptionFromMemberAttribute.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Diagnostics.CodeAnalysis; using System.Reflection; namespace McMaster.Extensions.CommandLineUtils @@ -39,7 +40,7 @@ public VersionOptionFromMemberAttribute(string template) /// public new string Template { get; set; } - internal CommandOption Configure(CommandLineApplication app, Type type, Func targetInstanceFactory) + internal CommandOption Configure(CommandLineApplication app, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type type, Func targetInstanceFactory) { Func? shortFormGetter = null; Func? longFormGetter = null; diff --git a/src/CommandLineUtils/CommandLineApplication.Execute.cs b/src/CommandLineUtils/CommandLineApplication.Execute.cs index 222b7c04..be38b711 100644 --- a/src/CommandLineUtils/CommandLineApplication.Execute.cs +++ b/src/CommandLineUtils/CommandLineApplication.Execute.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Threading; @@ -29,7 +30,7 @@ partial class CommandLineApplication /// A type that should be bound to the arguments. /// Thrown when attributes are incorrectly configured. /// The process exit code - public static int Execute(CommandLineContext context) + public static int Execute<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApp>(CommandLineContext context) where TApp : class => ExecuteAsync(context).GetAwaiter().GetResult(); @@ -47,7 +48,7 @@ public static int Execute(CommandLineContext context) /// A type that should be bound to the arguments. /// Thrown when attributes are incorrectly configured. /// The process exit code - public static async Task ExecuteAsync(CommandLineContext context, CancellationToken cancellationToken = default) + public static async Task ExecuteAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApp>(CommandLineContext context, CancellationToken cancellationToken = default) #pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters where TApp : class { @@ -109,7 +110,7 @@ public static async Task ExecuteAsync(CommandLineContext context, Can /// A type that should be bound to the arguments. /// Thrown when attributes are incorrectly configured. /// The process exit code - public static int Execute(params string[] args) + public static int Execute<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApp>(params string[] args) where TApp : class => Execute(PhysicalConsole.Singleton, args); @@ -126,7 +127,7 @@ public static int Execute(params string[] args) /// A type that should be bound to the arguments. /// Thrown when attributes are incorrectly configured. /// The process exit code - public static int Execute(IConsole console, params string[] args) + public static int Execute<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApp>(IConsole console, params string[] args) where TApp : class { args ??= Array.Empty(); @@ -146,7 +147,7 @@ public static int Execute(IConsole console, params string[] args) /// A type that should be bound to the arguments. /// Thrown when attributes are incorrectly configured. /// The process exit code - public static Task ExecuteAsync(params string[] args) + public static Task ExecuteAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApp>(params string[] args) where TApp : class => ExecuteAsync(PhysicalConsole.Singleton, args); @@ -164,7 +165,7 @@ public static Task ExecuteAsync(params string[] args) /// A type that should be bound to the arguments. /// Thrown when attributes are incorrectly configured. /// The process exit code - public static Task ExecuteAsync(string[] args, CancellationToken cancellationToken = default) + public static Task ExecuteAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApp>(string[] args, CancellationToken cancellationToken = default) #pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters where TApp : class { @@ -186,7 +187,7 @@ public static Task ExecuteAsync(string[] args, CancellationToken canc /// A type that should be bound to the arguments. /// Thrown when attributes are incorrectly configured. /// The process exit code - public static Task ExecuteAsync(IConsole console, params string[] args) + public static Task ExecuteAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TApp>(IConsole console, params string[] args) where TApp : class { args ??= Array.Empty(); diff --git a/src/CommandLineUtils/CommandLineApplication.cs b/src/CommandLineUtils/CommandLineApplication.cs index 5aa4cea3..1423cc81 100644 --- a/src/CommandLineUtils/CommandLineApplication.cs +++ b/src/CommandLineUtils/CommandLineApplication.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; @@ -108,7 +109,10 @@ internal CommandLineApplication(CommandLineApplication parent, string name) /// /// Constructor for subcommands with a known model type (used for AOT-compatible subcommand creation). /// - internal CommandLineApplication(CommandLineApplication parent, string name, Type modelType) + internal CommandLineApplication( + CommandLineApplication parent, + string name, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type modelType) : this(parent, parent._helpTextGenerator, parent._context, modelType) { if (name != null) @@ -129,7 +133,7 @@ internal CommandLineApplication( CommandLineApplication? parent, IHelpTextGenerator helpTextGenerator, CommandLineContext context, - Type? modelType) + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type? modelType) { _context = context ?? throw new ArgumentNullException(nameof(context)); Parent = parent; @@ -484,7 +488,7 @@ public void AddSubcommand(CommandLineApplication subcommand) /// The type of the model. /// Factory to create instances of the model. /// The created subcommand. - internal CommandLineApplication AddSubcommand(string name, Type modelType, SourceGeneration.IModelFactory modelFactory) + internal CommandLineApplication AddSubcommand(string name, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type modelType, SourceGeneration.IModelFactory modelFactory) { AssertCommandNameIsUnique(name, null); @@ -544,7 +548,7 @@ public CommandLineApplication Command(string name, ActionA callback used to configure the subcommand object. /// The model type of the subcommand. /// - public CommandLineApplication Command(string name, Action> configuration) + public CommandLineApplication Command<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TModel>(string name, Action> configuration) where TModel : class { var command = new CommandLineApplication(this, name); @@ -628,7 +632,7 @@ public void AddOption(CommandOption option) /// /// The type of the values on the option /// The option - public CommandOption Option(string template, string description, CommandOptionType optionType, Action> configuration, bool inherited) + public CommandOption Option<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(string template, string description, CommandOptionType optionType, Action> configuration, bool inherited) { var parser = ValueParsers.GetParser(); @@ -692,7 +696,7 @@ public CommandArgument Argument(string name, string description, Action /// The type of the values on the option /// - public CommandArgument Argument(string name, string description, Action> configuration, bool multipleValues = false) + public CommandArgument Argument<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(string name, string description, Action> configuration, bool multipleValues = false) #pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters { var parser = ValueParsers.GetParser(); @@ -1154,7 +1158,7 @@ IConventionBuilder IConventionBuilder.AddConvention(IConvention convention) /// public IConventionBuilder Conventions => _builder ??= new Builder(this); - private protected virtual ConventionContext CreateConventionContext(Type? modelType) => new(this, modelType); + private protected virtual ConventionContext CreateConventionContext([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type? modelType) => new(this, modelType); private bool _settingContext; internal void SetContext(CommandLineContext context) diff --git a/src/CommandLineUtils/CommandLineApplicationExtensions.cs b/src/CommandLineUtils/CommandLineApplicationExtensions.cs index d2960232..8be8a499 100644 --- a/src/CommandLineUtils/CommandLineApplicationExtensions.cs +++ b/src/CommandLineUtils/CommandLineApplicationExtensions.cs @@ -5,6 +5,7 @@ using System; using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Threading; using System.Threading.Tasks; @@ -24,7 +25,7 @@ public static class CommandLineApplicationExtensions /// /// /// - public static CommandArgument Argument(this CommandLineApplication app, string name, string description, bool multipleValues = false) + public static CommandArgument Argument<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(this CommandLineApplication app, string name, string description, bool multipleValues = false) => app.Argument(name, description, _ => { }, multipleValues); /// @@ -35,7 +36,7 @@ public static CommandArgument Argument(this CommandLineApplication app, st /// /// /// - public static CommandOption Option(this CommandLineApplication app, string template, string description, CommandOptionType optionType) + public static CommandOption Option<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(this CommandLineApplication app, string template, string description, CommandOptionType optionType) => app.Option(template, description, optionType, _ => { }, inherited: false); /// @@ -47,7 +48,7 @@ public static CommandOption Option(this CommandLineApplication app, string /// /// /// - public static CommandOption Option(this CommandLineApplication app, string template, string description, CommandOptionType optionType, bool inherited) + public static CommandOption Option<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(this CommandLineApplication app, string template, string description, CommandOptionType optionType, bool inherited) => app.Option(template, description, optionType, _ => { }, inherited); /// @@ -59,7 +60,7 @@ public static CommandOption Option(this CommandLineApplication app, string /// /// /// - public static CommandOption Option(this CommandLineApplication app, string template, string description, CommandOptionType optionType, Action> configuration) + public static CommandOption Option<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(this CommandLineApplication app, string template, string description, CommandOptionType optionType, Action> configuration) => app.Option(template, description, optionType, configuration, inherited: false); /// diff --git a/src/CommandLineUtils/CommandLineApplicationWithModel.cs b/src/CommandLineUtils/CommandLineApplicationWithModel.cs index 68e515da..cf33c20d 100644 --- a/src/CommandLineUtils/CommandLineApplicationWithModel.cs +++ b/src/CommandLineUtils/CommandLineApplicationWithModel.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Diagnostics.CodeAnalysis; using McMaster.Extensions.CommandLineUtils.Abstractions; using McMaster.Extensions.CommandLineUtils.SourceGeneration; @@ -13,6 +14,7 @@ namespace McMaster.Extensions.CommandLineUtils /// internal sealed class CommandLineApplicationWithModel : CommandLineApplication, IModelAccessor { + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] private readonly Type _modelType; private readonly IModelFactory _modelFactory; private readonly Lazy _lazyModel; @@ -20,7 +22,7 @@ internal sealed class CommandLineApplicationWithModel : CommandLineApplication, internal CommandLineApplicationWithModel( CommandLineApplication parent, string name, - Type modelType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type modelType, IModelFactory modelFactory) : base(parent, name, modelType) { @@ -34,6 +36,7 @@ internal CommandLineApplicationWithModel( /// public object Model => _lazyModel.Value; + [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type IModelAccessor.GetModelType() => _modelType; object IModelAccessor.GetModel() => Model; diff --git a/src/CommandLineUtils/CommandLineApplication{T}.cs b/src/CommandLineUtils/CommandLineApplication{T}.cs index 73fd1e1f..5ecb1807 100644 --- a/src/CommandLineUtils/CommandLineApplication{T}.cs +++ b/src/CommandLineUtils/CommandLineApplication{T}.cs @@ -13,7 +13,7 @@ namespace McMaster.Extensions.CommandLineUtils /// Describes a set of command line arguments, options, and execution behavior /// using a type of to model the application. /// - public class CommandLineApplication : CommandLineApplication, IModelAccessor + public class CommandLineApplication<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TModel> : CommandLineApplication, IModelAccessor where TModel : class { [AllowNull] // this is initialized in common method called in all constructors @@ -96,6 +96,7 @@ private static TModel DefaultModelFactory() /// public TModel Model => _lazy.Value; + [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type IModelAccessor.GetModelType() => typeof(TModel); object IModelAccessor.GetModel() => Model; @@ -115,7 +116,7 @@ public Func ModelFactory set => _modelFactory = value ?? throw new ArgumentNullException(nameof(value)); } - private protected override ConventionContext CreateConventionContext(Type? modelType) => new(this, typeof(TModel)); + private protected override ConventionContext CreateConventionContext([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type? modelType) => new(this, typeof(TModel)); /// public override void Dispose() diff --git a/src/CommandLineUtils/Conventions/ConstructorInjectionConvention.cs b/src/CommandLineUtils/Conventions/ConstructorInjectionConvention.cs index 406de96f..be72e26c 100644 --- a/src/CommandLineUtils/Conventions/ConstructorInjectionConvention.cs +++ b/src/CommandLineUtils/Conventions/ConstructorInjectionConvention.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; @@ -32,6 +33,10 @@ public ConstructorInjectionConvention(IServiceProvider additionalServices) } /// + [UnconditionalSuppressMessage("Trimming", "IL2060", + Justification = "context.ModelType is annotated with DynamicallyAccessedMemberTypes.All, so the " + + "members ApplyImpl needs from it are preserved even though MakeGenericMethod's own use " + + "of context.ModelType cannot be statically verified by the trimmer.")] public virtual void Apply(ConventionContext context) { if (_additionalServices != null) @@ -50,7 +55,7 @@ public virtual void Apply(ConventionContext context) private static readonly MethodInfo s_applyMethod = typeof(ConstructorInjectionConvention).GetRuntimeMethods().Single(m => m.Name == nameof(ApplyImpl)); - private void ApplyImpl(ConventionContext context) + private void ApplyImpl<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TModel>(ConventionContext context) where TModel : class { var constructors = typeof(TModel).GetConstructors(BindingFlags.Public | BindingFlags.Instance); @@ -64,7 +69,7 @@ private void ApplyImpl(ConventionContext context) } } - private static Func? FindMatchedConstructor( + private static Func? FindMatchedConstructor<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TModel>( ConstructorInfo[] constructors, IServiceProvider services, bool throwIfNoParameterTypeRegistered = false) diff --git a/src/CommandLineUtils/Conventions/ConventionContext.cs b/src/CommandLineUtils/Conventions/ConventionContext.cs index 8d1a059d..bb7952e9 100644 --- a/src/CommandLineUtils/Conventions/ConventionContext.cs +++ b/src/CommandLineUtils/Conventions/ConventionContext.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Diagnostics.CodeAnalysis; using McMaster.Extensions.CommandLineUtils.Abstractions; using McMaster.Extensions.CommandLineUtils.SourceGeneration; @@ -19,7 +20,9 @@ public class ConventionContext /// /// The application /// The type of the model. - public ConventionContext(CommandLineApplication application, Type? modelType) + public ConventionContext( + CommandLineApplication application, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type? modelType) { Application = application ?? throw new ArgumentNullException(nameof(application)); ModelType = modelType; @@ -34,6 +37,7 @@ public ConventionContext(CommandLineApplication application, Type? modelType) /// The type of the application model. Can be null when applied to /// instead of . /// + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] public Type? ModelType { get; private set; } /// @@ -49,6 +53,12 @@ public ConventionContext(CommandLineApplication application, Type? modelType) /// This provider may contain source-generated metadata (AOT-friendly) /// or fall back to reflection-based metadata extraction. /// + [UnconditionalSuppressMessage("Trimming", "IL2026", + Justification = "Falls back to reflection only when the source generator has not produced metadata " + + "for ModelType (e.g. the McMaster.Extensions.CommandLineUtils.Generators analyzer was not " + + "referenced, or the build did not set PublishAot). ModelType itself is annotated with " + + "DynamicallyAccessedMemberTypes.All, which preserves the members the reflection-based " + + "provider needs.")] public ICommandMetadataProvider? MetadataProvider { get diff --git a/src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs b/src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs index d9c484a5..5cea05e6 100644 --- a/src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs +++ b/src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using McMaster.Extensions.CommandLineUtils.Validation; @@ -17,6 +18,11 @@ namespace McMaster.Extensions.CommandLineUtils.Conventions public abstract class OptionAttributeConventionBase where TAttribute : OptionAttributeBase { + [UnconditionalSuppressMessage("Trimming", "IL2072", + Justification = "prop.PropertyType is not itself annotated (PropertyInfo.PropertyType has no " + + "DynamicallyAccessedMembers support), but prop was obtained from ConventionContext.ModelType, " + + "which is annotated with DynamicallyAccessedMemberTypes.All, so prop's declaring type - and " + + "therefore prop's own type - is preserved by trimming.")] private protected void AddOption(ConventionContext context, CommandOption option, PropertyInfo prop) { var modelAccessor = context.ModelAccessor; diff --git a/src/CommandLineUtils/Conventions/SubcommandAttributeConvention.cs b/src/CommandLineUtils/Conventions/SubcommandAttributeConvention.cs index db527017..1a646903 100644 --- a/src/CommandLineUtils/Conventions/SubcommandAttributeConvention.cs +++ b/src/CommandLineUtils/Conventions/SubcommandAttributeConvention.cs @@ -17,6 +17,10 @@ namespace McMaster.Extensions.CommandLineUtils.Conventions public class SubcommandAttributeConvention : IConvention { /// + [UnconditionalSuppressMessage("Trimming", "IL2026", + Justification = "Falls back to reflection only when a subcommand type has no source-generated or " + + "factory-provided metadata. AddSubcommandFromMetadata annotates subcommandType with the " + + "constructor accessed member types it needs, so the members it reflects over are preserved.")] public virtual void Apply(ConventionContext context) { // MetadataProvider is always available (generated or reflection-based via DefaultMetadataResolver) @@ -58,7 +62,7 @@ private static string GetSubcommandName(Type subcommandType, ICommandMetadataPro private void AddSubcommandFromMetadata( ConventionContext context, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type subcommandType, ICommandMetadataProvider provider, string name) diff --git a/src/CommandLineUtils/Internal/CollectionParserProvider.cs b/src/CommandLineUtils/Internal/CollectionParserProvider.cs index 3f336221..d67bbbb2 100644 --- a/src/CommandLineUtils/Internal/CollectionParserProvider.cs +++ b/src/CommandLineUtils/Internal/CollectionParserProvider.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using McMaster.Extensions.CommandLineUtils.Abstractions; @@ -15,7 +16,12 @@ private CollectionParserProvider() public static CollectionParserProvider Default { get; } = new CollectionParserProvider(); - public ICollectionParser? GetParser(Type type, ValueParserProvider valueParsers) + [UnconditionalSuppressMessage("Trimming", "IL2072", + Justification = "type is annotated with DynamicallyAccessedMemberTypes.All, so its element/generic " + + "argument type (an array element or the sole type argument of a single-generic-parameter " + + "collection interface/List<>) is reachable through it, even though Type.GetElementType()/" + + "GetGenericArguments() do not themselves carry a matching annotation on their return value.")] + public ICollectionParser? GetParser([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type type, ValueParserProvider valueParsers) { if (type.IsArray) { diff --git a/src/CommandLineUtils/Internal/CommandLineValidationContext.cs b/src/CommandLineUtils/Internal/CommandLineValidationContext.cs index ad598ebf..0dfe2f7b 100644 --- a/src/CommandLineUtils/Internal/CommandLineValidationContext.cs +++ b/src/CommandLineUtils/Internal/CommandLineValidationContext.cs @@ -3,6 +3,7 @@ using System; using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; namespace McMaster.Extensions.CommandLineUtils { @@ -15,10 +16,20 @@ public CommandLineValidationContextFactory(CommandLineApplication app) _app = app ?? throw new ArgumentNullException(nameof(app)); } + [UnconditionalSuppressMessage("Trimming", "IL2026", + Justification = "ValidationContext's constructor requires unreferenced code because the instance's " + + "type cannot be statically discovered in general. This library never passes the resulting " + + "context to Validator.TryValidateObject/ValidateObject (which is what would reflect over the " + + "instance's properties); it only reads ValidationContext.DisplayName/MemberName, which do not " + + "require reflection over the instance's type.")] public ValidationContext Create(CommandLineApplication app) => new(app, _app, null); + [UnconditionalSuppressMessage("Trimming", "IL2026", + Justification = "See CommandLineValidationContextFactory.Create(CommandLineApplication).")] public ValidationContext Create(CommandArgument argument) => new(argument, _app, null); + [UnconditionalSuppressMessage("Trimming", "IL2026", + Justification = "See CommandLineValidationContextFactory.Create(CommandLineApplication).")] public ValidationContext Create(CommandOption option) => new(option, _app, null); } } diff --git a/src/CommandLineUtils/Internal/CommandOptionTypeMapper.cs b/src/CommandLineUtils/Internal/CommandOptionTypeMapper.cs index 7b5622ef..a07675ae 100644 --- a/src/CommandLineUtils/Internal/CommandOptionTypeMapper.cs +++ b/src/CommandLineUtils/Internal/CommandOptionTypeMapper.cs @@ -3,6 +3,7 @@ using System; using System.Collections; +using System.Diagnostics.CodeAnalysis; using McMaster.Extensions.CommandLineUtils.Abstractions; namespace McMaster.Extensions.CommandLineUtils @@ -15,7 +16,7 @@ private CommandOptionTypeMapper() public static CommandOptionTypeMapper Default { get; } = new CommandOptionTypeMapper(); public bool TryGetOptionType( - Type clrType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type clrType, ValueParserProvider valueParsers, out CommandOptionType optionType) { @@ -31,7 +32,14 @@ public bool TryGetOptionType( } } - public CommandOptionType GetOptionType(Type clrType, ValueParserProvider? valueParsers = null) + [UnconditionalSuppressMessage("Trimming", "IL2072", + Justification = "wrappedType/wrappedType2/wrappedType3 are generic type arguments extracted from " + + "clrType (Nullable/ValueTuple/Tuple) via ReflectionHelper's Is*Type helpers, " + + "which do not themselves propagate DynamicallyAccessedMembers annotations. clrType is annotated " + + "with DynamicallyAccessedMemberTypes.All, and preserving a closed generic type also preserves " + + "its type arguments' members.")] + [UnconditionalSuppressMessage("Trimming", "IL2067", Justification = "See the IL2072 suppression above.")] + public CommandOptionType GetOptionType([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type clrType, ValueParserProvider? valueParsers = null) { if (clrType == typeof(bool) || clrType == typeof(bool[])) { diff --git a/src/CommandLineUtils/Internal/ReflectionHelper.cs b/src/CommandLineUtils/Internal/ReflectionHelper.cs index b60f7ca5..b48428b4 100644 --- a/src/CommandLineUtils/Internal/ReflectionHelper.cs +++ b/src/CommandLineUtils/Internal/ReflectionHelper.cs @@ -23,11 +23,19 @@ internal static class ReflectionHelper // Microsoft.Extensions.DependencyInjection.Abstractions is present at runtime). // By resolving these via reflection the core library avoids a hard dependency on // the DI abstractions package. + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] private static readonly Type? s_fromKeyedServicesAttributeType; private static readonly PropertyInfo? s_keyProperty; + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] private static readonly Type? s_keyedServiceProviderType; private static readonly MethodInfo? s_getKeyedServiceMethod; + [UnconditionalSuppressMessage("Trimming", "IL2065", + Justification = "Type.GetType is used to softly-detect the optional " + + "Microsoft.Extensions.DependencyInjection.Abstractions package at runtime by its well-known, " + + "fixed type names. When that package is trimmed away, Type.GetType returns null and keyed-service " + + "support is simply disabled (see the null checks below); when it is present and referenced by the " + + "app, its public surface is preserved because the app itself references these types.")] static ReflectionHelper() { s_fromKeyedServicesAttributeType = Type.GetType( @@ -49,6 +57,11 @@ static ReflectionHelper() } } + [UnconditionalSuppressMessage("Trimming", "IL2075", + Justification = "prop.DeclaringType is not itself annotated (PropertyInfo.DeclaringType has no " + + "DynamicallyAccessedMembers support), but prop was obtained by reflecting over a model type " + + "that flows from an annotated (DynamicallyAccessedMemberTypes.All) entry point, so its backing " + + "field is preserved by trimming.")] public static SetPropertyDelegate GetPropertySetter(PropertyInfo prop) { var setter = prop.GetSetMethod(nonPublic: true); @@ -71,6 +84,8 @@ public static SetPropertyDelegate GetPropertySetter(PropertyInfo prop) } } + [UnconditionalSuppressMessage("Trimming", "IL2075", + Justification = "See GetPropertySetter.")] public static GetPropertyDelegate GetPropertyGetter(PropertyInfo prop) { var getter = prop.GetGetMethod(nonPublic: true); @@ -97,7 +112,7 @@ public static GetPropertyDelegate GetPropertyGetter(PropertyInfo prop) } } - public static MethodInfo[] GetPropertyOrMethod(Type type, string name) + public static MethodInfo[] GetPropertyOrMethod([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type type, string name) { var members = GetAllMembers(type).ToList(); #pragma warning disable CS8602 // Dereference of a possibly null reference. @@ -112,14 +127,14 @@ public static MethodInfo[] GetPropertyOrMethod(Type type, string name) #pragma warning restore CS8602 // Dereference of a possibly null reference. } - public static PropertyInfo[] GetProperties(Type type) + public static PropertyInfo[] GetProperties([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type type) { return GetAllMembers(type) .OfType() .ToArray(); } - public static MemberInfo[] GetMembers(Type type) + public static MemberInfo[] GetMembers([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type type) { return GetAllMembers(type).ToArray(); } @@ -309,7 +324,12 @@ public int GetHashCode(MethodInfo obj) } } - private static IEnumerable GetAllMembers(Type type) + [UnconditionalSuppressMessage("Trimming", "IL2075", + Justification = "currentType walks the DynamicallyAccessedMemberTypes.All-annotated type's base " + + "type chain. Type.BaseType's return value is not itself annotated, but preserving a type's " + + "members (via the All annotation on the original 'type' argument) also preserves its base " + + "types' members, so the members read here are not trimmed away.")] + private static IEnumerable GetAllMembers([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type type) { // Keep track of the base definitions of property getters we see so we can skip ones we've seen already var baseGetters = new HashSet(new MethodMetadataEquality()); diff --git a/src/CommandLineUtils/Internal/ValueParsers/HashSetParser.cs b/src/CommandLineUtils/Internal/ValueParsers/HashSetParser.cs index 9951b054..a68d4603 100644 --- a/src/CommandLineUtils/Internal/ValueParsers/HashSetParser.cs +++ b/src/CommandLineUtils/Internal/ValueParsers/HashSetParser.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Reflection; @@ -11,12 +12,17 @@ namespace McMaster.Extensions.CommandLineUtils.Abstractions internal class HashSetParser : ICollectionParser { private readonly IValueParser _elementParser; + + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicMethods)] private readonly Type _listType; private readonly MethodInfo _addMethod; private readonly CultureInfo _parserCulture; #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. - public HashSetParser(Type elementType, IValueParser elementParser, CultureInfo parserCulture) + public HashSetParser( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicMethods)] Type elementType, + IValueParser elementParser, + CultureInfo parserCulture) #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. { _elementParser = elementParser; diff --git a/src/CommandLineUtils/Internal/ValueParsers/ListParser.cs b/src/CommandLineUtils/Internal/ValueParsers/ListParser.cs index d7ceaf8b..047782a1 100644 --- a/src/CommandLineUtils/Internal/ValueParsers/ListParser.cs +++ b/src/CommandLineUtils/Internal/ValueParsers/ListParser.cs @@ -4,6 +4,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; namespace McMaster.Extensions.CommandLineUtils.Abstractions @@ -12,10 +13,15 @@ namespace McMaster.Extensions.CommandLineUtils.Abstractions internal class ListParser : ICollectionParser { private readonly IValueParser _elementParser; + + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] private readonly Type _listType; private readonly CultureInfo _parserCulture; - public ListParser(Type elementType, IValueParser elementParser, CultureInfo parserCulture) + public ListParser( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type elementType, + IValueParser elementParser, + CultureInfo parserCulture) { _elementParser = elementParser; _listType = typeof(List<>).MakeGenericType(elementType); diff --git a/src/CommandLineUtils/Internal/ValueParsers/TypeDescriptorValueParserFactory.cs b/src/CommandLineUtils/Internal/ValueParsers/TypeDescriptorValueParserFactory.cs index 930b1339..a002d166 100644 --- a/src/CommandLineUtils/Internal/ValueParsers/TypeDescriptorValueParserFactory.cs +++ b/src/CommandLineUtils/Internal/ValueParsers/TypeDescriptorValueParserFactory.cs @@ -14,7 +14,10 @@ namespace McMaster.Extensions.CommandLineUtils.Abstractions /// internal class TypeDescriptorValueParserFactory { - public bool TryGetParser([NotNullWhen(true)] out IValueParser? parser) + [RequiresUnreferencedCode("Uses System.ComponentModel.TypeDescriptor.GetConverter, which requires " + + "unreferenced code. This is a fallback used only for option/argument types that are not one of " + + "the library's built-in value parsers (primitives, enums, DateTime, Uri, etc.).")] + public bool TryGetParser<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>([NotNullWhen(true)] out IValueParser? parser) { var targetType = typeof(T); var converter = TypeDescriptor.GetConverter(targetType); diff --git a/src/CommandLineUtils/McMaster.Extensions.CommandLineUtils.csproj b/src/CommandLineUtils/McMaster.Extensions.CommandLineUtils.csproj index 63f34efd..1b0e44da 100644 --- a/src/CommandLineUtils/McMaster.Extensions.CommandLineUtils.csproj +++ b/src/CommandLineUtils/McMaster.Extensions.CommandLineUtils.csproj @@ -4,6 +4,13 @@ net8.0;net472 true true + + true + true Command-line parsing API. Command-line parsing API and utilities for console applications. diff --git a/src/CommandLineUtils/SourceGeneration/IMetadataResolver.cs b/src/CommandLineUtils/SourceGeneration/IMetadataResolver.cs index 7b6c823c..7dc9e4cc 100644 --- a/src/CommandLineUtils/SourceGeneration/IMetadataResolver.cs +++ b/src/CommandLineUtils/SourceGeneration/IMetadataResolver.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Diagnostics.CodeAnalysis; namespace McMaster.Extensions.CommandLineUtils.SourceGeneration { @@ -16,6 +17,7 @@ public interface IMetadataResolver /// /// The model type. /// A metadata provider for the type. + [RequiresUnreferencedCode("Falls back to reflection when no generated metadata is available. Use the source generator for AOT compatibility.")] ICommandMetadataProvider GetProvider(Type modelType); /// @@ -24,6 +26,7 @@ public interface IMetadataResolver /// /// The model type. /// A metadata provider for the type. + [RequiresUnreferencedCode("Falls back to reflection when no generated metadata is available. Use the source generator for AOT compatibility.")] ICommandMetadataProvider GetProvider() where TModel : class; /// diff --git a/src/CommandLineUtils/SourceGeneration/Metadata/ArgumentMetadata.cs b/src/CommandLineUtils/SourceGeneration/Metadata/ArgumentMetadata.cs index a3f72a02..0b971594 100644 --- a/src/CommandLineUtils/SourceGeneration/Metadata/ArgumentMetadata.cs +++ b/src/CommandLineUtils/SourceGeneration/Metadata/ArgumentMetadata.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; namespace McMaster.Extensions.CommandLineUtils.SourceGeneration { @@ -22,7 +23,7 @@ public sealed class ArgumentMetadata /// Delegate to set the property value on a model instance. public ArgumentMetadata( string propertyName, - Type propertyType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type propertyType, int order, Func getter, Action setter) @@ -42,6 +43,7 @@ public ArgumentMetadata( /// /// The CLR type of the property. /// + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] public Type PropertyType { get; } /// diff --git a/src/CommandLineUtils/SourceGeneration/Metadata/OptionMetadata.cs b/src/CommandLineUtils/SourceGeneration/Metadata/OptionMetadata.cs index cb087fb5..1f04c540 100644 --- a/src/CommandLineUtils/SourceGeneration/Metadata/OptionMetadata.cs +++ b/src/CommandLineUtils/SourceGeneration/Metadata/OptionMetadata.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; namespace McMaster.Extensions.CommandLineUtils.SourceGeneration { @@ -21,7 +22,7 @@ public sealed class OptionMetadata /// Delegate to set the property value on a model instance. public OptionMetadata( string propertyName, - Type propertyType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type propertyType, Func getter, Action setter) { @@ -39,6 +40,7 @@ public OptionMetadata( /// /// The CLR type of the property. /// + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] public Type PropertyType { get; } /// diff --git a/src/CommandLineUtils/SourceGeneration/Metadata/SubcommandMetadata.cs b/src/CommandLineUtils/SourceGeneration/Metadata/SubcommandMetadata.cs index 47e0f509..90f392c7 100644 --- a/src/CommandLineUtils/SourceGeneration/Metadata/SubcommandMetadata.cs +++ b/src/CommandLineUtils/SourceGeneration/Metadata/SubcommandMetadata.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Diagnostics.CodeAnalysis; namespace McMaster.Extensions.CommandLineUtils.SourceGeneration { @@ -14,7 +15,9 @@ public sealed class SubcommandMetadata /// Initializes a new instance of the class. /// /// The type of the subcommand. - public SubcommandMetadata(Type subcommandType) + public SubcommandMetadata( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] + Type subcommandType) { SubcommandType = subcommandType ?? throw new ArgumentNullException(nameof(subcommandType)); } @@ -22,6 +25,7 @@ public SubcommandMetadata(Type subcommandType) /// /// The type of the subcommand. /// + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] public Type SubcommandType { get; } /// diff --git a/src/CommandLineUtils/Validation/ValidationExtensions.cs b/src/CommandLineUtils/Validation/ValidationExtensions.cs index 4dbdadaa..45165910 100644 --- a/src/CommandLineUtils/Validation/ValidationExtensions.cs +++ b/src/CommandLineUtils/Validation/ValidationExtensions.cs @@ -3,6 +3,7 @@ using System; using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; using McMaster.Extensions.CommandLineUtils.Validation; namespace McMaster.Extensions.CommandLineUtils @@ -333,6 +334,10 @@ public static IValidationBuilder LegalFilePath(this IValidationBuilder builder, /// The minimum length. /// A custom error message to display. /// The builder. + [UnconditionalSuppressMessage("Trimming", "IL2026", + Justification = "MinLengthAttribute's IsValid uses reflection to find a 'Count' property only on " + + "values that are not already ICollection/ICollection. This library uses MinLength for string " + + "and collection option/argument values, whose Count/Length members are not trimmed.")] public static IValidationBuilder MinLength(this IValidationBuilder builder, int length, string? errorMessage = null) => builder.Satisfies(new MinLengthAttribute(length), errorMessage); @@ -343,6 +348,10 @@ public static IValidationBuilder MinLength(this IValidationBuilder builder, int /// The maximum length. /// A custom error message to display. /// The builder. + [UnconditionalSuppressMessage("Trimming", "IL2026", + Justification = "MaxLengthAttribute's IsValid uses reflection to find a 'Count' property only on " + + "values that are not already ICollection/ICollection. This library uses MaxLength for string " + + "and collection option/argument values, whose Count/Length members are not trimmed.")] public static IValidationBuilder MaxLength(this IValidationBuilder builder, int length, string? errorMessage = null) => builder.Satisfies(new MaxLengthAttribute(length), errorMessage); @@ -364,7 +373,7 @@ public static IValidationBuilder RegularExpression(this IValidationBuilder build /// Constructor arguments for . /// A custom error message to display. /// The builder. - public static IValidationBuilder Satisfies(this IValidationBuilder builder, string? errorMessage = null, params object[] ctorArgs) + public static IValidationBuilder Satisfies<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TAttribute>(this IValidationBuilder builder, string? errorMessage = null, params object[] ctorArgs) where TAttribute : ValidationAttribute { var attribute = GetValidationAttr(errorMessage, ctorArgs); @@ -452,7 +461,7 @@ private static IValidationBuilder Satisfies(this IValidationBuilder builder, Val return builder; } - private static T GetValidationAttr(string? errorMessage, object[]? ctorArgs = null) + private static T GetValidationAttr<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T>(string? errorMessage, object[]? ctorArgs = null) where T : ValidationAttribute { #pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type.