Repository navigation
fix: annotate reflection call graph for trimming/NativeAOT compatibility - #616
Open
akashchamp wants to merge 1 commit into
Open
akashchamp wants to merge 1 commit into
akashchamp wants to merge 1 commit into
Conversation
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<TApp> and CommandLineApplication<TModel> 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<TApp>/ExecuteAsync<TApp> APIs, CommandLineApplication<TModel>, 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 natemcmaster#539
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #616 +/- ##
==========================================
+ Coverage 80.08% 80.13% +0.04%
==========================================
Files 120 120
Lines 4063 4073 +10
Branches 880 880
==========================================
+ Hits 3254 3264 +10
Misses 621 621
Partials 188 188 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
akashchamp
force-pushed
the
fix/trim-reflection-annotations-539
branch
from
September 25, 2026 18:18
80f59ca to
4b83335
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #539.
Problem
Publishing an app that uses
CommandLineApplication.Execute<TApp>withPublishTrimmed=true(orPublishAot=true) either warns during publish (oncetrim analysis is enabled) or throws at run time, e.g.:
The trimmer removes the constructors/methods/properties that
Execute<TApp>,CommandLineApplication<TModel>, and the attribute conventions discover viareflection, because none of that reflection was previously annotated with
DynamicallyAccessedMembers.What this PR does
Following the direction in the maintainer's own WIP commit
(3e0a482),
this adds
System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersannotations along the reflection call graph reachable from the library's
public entry points, so the trimmer preserves the members those code paths
actually use:
CommandLineApplication.Execute<TApp>/ExecuteAsync<TApp>(all sixoverloads) and
CommandLineApplication<TModel>annotate their model typeparameter with
DynamicallyAccessedMemberTypes.All, and that flows throughConventionContext.ModelType,ConstructorInjectionConvention, theattribute conventions (
SubcommandAttributeConvention,OptionAttributeConventionBase, etc.),ValueParserProvider.GetParser,CommandOptionTypeMapper, andReflectionHelper.IModelAccessor.GetModelType(),SubcommandMetadata.SubcommandType,OptionMetadata.PropertyType/ArgumentMetadata.PropertyType, andIMetadataResolver.GetProviderare annotated so metadata already flowingthrough the newer source-generator-aware types (
ICommandMetadataProvider,DefaultMetadataResolver) carries the same guarantees.HashSetParser/ListParserand the validation helpers(
ValidationExtensions.Satisfies<TAttribute>/GetValidationAttr<T>)annotate the
Types they construct viaActivator.CreateInstance.<IsTrimmable>/<EnableTrimAnalyzer>are now set for thenet8.0targetin
McMaster.Extensions.CommandLineUtils.csproj(guarded to that TFM,since
net472doesn't support trimming), so a future PR that reintroducesan unannotated reflection call will fail the normal build, not just a
trimmed publish.
TypeConverter-based value parsing (TypeDescriptorValueParserFactory),MinLengthAttribute/MaxLengthAttribute'sCount-property lookup,ValidationContext's constructor, andDefaultMetadataResolver'sreflection fallback when the source generator hasn't produced metadata for
a type. These are documented with
UnconditionalSuppressMessageand ajustification, the same pattern already used by
DefaultMetadataResolverin this codebase and by the BCL's own trimmable libraries.
New sample / regression check
Adds
docs/samples/trimming, completing the sample the maintainer started inthe WIP commit above. It publishes self-contained, single-file and trimmed
for
linux-x64, withEnableTrimAnalyzerandTreatWarningsAsErrorsset, soa
dotnet publishof the sample now fails the build if any reflection in thecall graph it exercises isn't properly annotated — not just a manual demo.
It's wired into
docs/samples/samples.sln, whichbuild.ps1already builds.Testing
All commands run against this branch on Linux (the CI matrix also covers
Windows/macOS):
dotnet build CommandLineUtils.sln -c Release— 0 warnings, 0 errors(net472 and net8.0, with the net8.0 target's
EnableTrimAnalyzercatchingevery reflection call this PR touches).
dotnet test(bothCommandLineUtils.TestsandHosting.CommandLine.Tests, net8.0 and net10.0) — all 1125 + 18 existingtests pass unchanged; no tests were removed or weakened.
pwsh -File build.ps1 -ci(the project's own CI script: formatverification, full build with
-warnaserror:CS1591, pack, the samplessolution build, and the full test run) — succeeds end to end.
warn, once analysis was on, or the app would throw at run time), and the
resulting self-contained trimmed binary runs correctly:
[Option]binding (including defaultvalues),
OnExecute, and generated help text all survive trimming.Scope notes
This is scoped to the reflection paths that
Execute<TApp>and the attributeconventions still use today (the same surface the linked WIP commit started
annotating), not a rewrite of the separate source-generator/AOT work already
in
src/CommandLineUtils.Generators.docs/samples/aot-samplecurrentlysuppresses trim/AOT warnings wholesale
(
SuppressTrimAnalysisWarnings/EnableTrimAnalyzer=false) with a commentthat "the library still uses reflection internally" — this PR doesn't change
that sample, since fully wiring the generator into
Execute<TApp>'s defaultpath is a larger, separate change; happy to look at that as a follow-up if
useful.