From 4b83335f61460c7381da9f2f8659c5fcf7bf322d Mon Sep 17 00:00:00 2001 From: Akash Kumar <116457960+akashchamp@users.noreply.github.com> Date: Thu, 24 Sep 2026 06:25:45 +0530 Subject: [PATCH] fix: annotate reflection call graph for trimming/NativeAOT compatibility Trimmed and self-contained single-file publishes (PublishTrimmed=true) crash at run time with exceptions like "No method named OnExecute or OnExecuteAsync could be found" and "Could not find any public constructors of type X", because the ILLink trimmer cannot see that CommandLineApplication.Execute and CommandLineApplication depend on TApp/TModel's constructors, methods and properties via reflection. Add System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers annotations along the reflection call graph reachable from the public Execute/ExecuteAsync APIs, CommandLineApplication, the attribute conventions, value parsers and validation helpers, so the trimmer preserves the members those code paths actually use. Enable IsTrimmable/EnableTrimAnalyzer for the net8.0 target so this is checked on every build (net472 does not support trimming). The handful of call sites that are irreducibly reflection-based (custom TypeConverter-based value parsing, DataAnnotations MinLength/ MaxLength, ValidationContext construction, and the metadata resolver's reflection fallback when the source generator has not run) are documented with UnconditionalSuppressMessage instead, matching how those APIs are already annotated upstream. Add docs/samples/trimming, a sample that publishes self-contained and trimmed for linux-x64 with EnableTrimAnalyzer/TreatWarningsAsErrors enabled, so the sample itself acts as a regression check; it is wired into docs/samples/samples.sln so the normal build catches regressions. Fixes #539 --- docs/samples/samples.sln | 17 ++++++++++ docs/samples/trimming/Program.cs | 25 ++++++++++++++ docs/samples/trimming/README.md | 29 ++++++++++++++++ docs/samples/trimming/TrimmingSample.csproj | 26 +++++++++++++++ .../Abstractions/IModelAccessor.cs | 2 ++ .../Abstractions/ValueParserProvider.cs | 33 ++++++++++++++++--- .../Attributes/OptionAttribute.cs | 6 ++++ .../VersionOptionFromMemberAttribute.cs | 3 +- .../CommandLineApplication.Execute.cs | 15 +++++---- .../CommandLineApplication.cs | 18 ++++++---- .../CommandLineApplicationExtensions.cs | 9 ++--- .../CommandLineApplicationWithModel.cs | 5 ++- .../CommandLineApplication{T}.cs | 5 +-- .../ConstructorInjectionConvention.cs | 9 +++-- .../Conventions/ConventionContext.cs | 12 ++++++- .../OptionAttributeConventionBase.cs | 6 ++++ .../SubcommandAttributeConvention.cs | 6 +++- .../Internal/CollectionParserProvider.cs | 8 ++++- .../Internal/CommandLineValidationContext.cs | 11 +++++++ .../Internal/CommandOptionTypeMapper.cs | 12 +++++-- .../Internal/ReflectionHelper.cs | 28 +++++++++++++--- .../Internal/ValueParsers/HashSetParser.cs | 8 ++++- .../Internal/ValueParsers/ListParser.cs | 8 ++++- .../TypeDescriptorValueParserFactory.cs | 5 ++- ...cMaster.Extensions.CommandLineUtils.csproj | 7 ++++ .../SourceGeneration/IMetadataResolver.cs | 3 ++ .../Metadata/ArgumentMetadata.cs | 4 ++- .../Metadata/OptionMetadata.cs | 4 ++- .../Metadata/SubcommandMetadata.cs | 6 +++- .../Validation/ValidationExtensions.cs | 13 ++++++-- 30 files changed, 297 insertions(+), 46 deletions(-) create mode 100644 docs/samples/trimming/Program.cs create mode 100644 docs/samples/trimming/README.md create mode 100644 docs/samples/trimming/TrimmingSample.csproj 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.