Skip to content

fix: annotate reflection call graph for trimming/NativeAOT compatibility - #616

Open
akashchamp wants to merge 1 commit into
natemcmaster:mainfrom
akashchamp:fix/trim-reflection-annotations-539
Open

akashchamp wants to merge 1 commit into
natemcmaster:mainfrom
akashchamp:fix/trim-reflection-annotations-539

Conversation

@akashchamp

@akashchamp akashchamp commented Sep 24, 2026 •

Copy link
Copy Markdown

Fixes #539.

Problem

Publishing an app that uses CommandLineApplication.Execute<TApp> with
PublishTrimmed=true (or PublishAot=true) either warns during publish (once
trim analysis is enabled) or throws at run time, e.g.:

Unhandled exception. System.InvalidOperationException: No method named 'OnExecute' or 'OnExecuteAsync' could be found.
Unhandled exception. System.InvalidOperationException: Could not find any public constructors of type 'MyCommand'.

The trimmer removes the constructors/methods/properties that Execute<TApp>,
CommandLineApplication<TModel>, and the attribute conventions discover via
reflection, 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.DynamicallyAccessedMembers
annotations 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 six
    overloads) and CommandLineApplication<TModel> annotate their model type
    parameter with DynamicallyAccessedMemberTypes.All, and that flows through
    ConventionContext.ModelType, ConstructorInjectionConvention, the
    attribute conventions (SubcommandAttributeConvention,
    OptionAttributeConventionBase, etc.), ValueParserProvider.GetParser,
    CommandOptionTypeMapper, and ReflectionHelper.
  • IModelAccessor.GetModelType(), SubcommandMetadata.SubcommandType,
    OptionMetadata.PropertyType/ArgumentMetadata.PropertyType, and
    IMetadataResolver.GetProvider are annotated so metadata already flowing
    through the newer source-generator-aware types (ICommandMetadataProvider,
    DefaultMetadataResolver) carries the same guarantees.
  • HashSetParser/ListParser and the validation helpers
    (ValidationExtensions.Satisfies<TAttribute>/GetValidationAttr<T>)
    annotate the Types they construct via Activator.CreateInstance.
  • The library opts into trim analysis itself:
    <IsTrimmable>/<EnableTrimAnalyzer> are now set for the net8.0 target
    in McMaster.Extensions.CommandLineUtils.csproj (guarded to that TFM,
    since net472 doesn't support trimming), so a future PR that reintroduces
    an unannotated reflection call will fail the normal build, not just a
    trimmed publish.
  • A small number of call sites are irreducibly reflection-based — custom
    TypeConverter-based value parsing (TypeDescriptorValueParserFactory),
    MinLengthAttribute/MaxLengthAttribute's Count-property lookup,
    ValidationContext's constructor, and DefaultMetadataResolver's
    reflection fallback when the source generator hasn't produced metadata for
    a type. These are documented with UnconditionalSuppressMessage and a
    justification, the same pattern already used by DefaultMetadataResolver
    in this codebase and by the BCL's own trimmable libraries.

New sample / regression check

Adds docs/samples/trimming, completing the sample the maintainer started in
the WIP commit above. It publishes self-contained, single-file and trimmed
for linux-x64, with EnableTrimAnalyzer and TreatWarningsAsErrors set, so
a dotnet publish of the sample now fails the build if any reflection in the
call graph it exercises isn't properly annotated — not just a manual demo.
It's wired into docs/samples/samples.sln, which build.ps1 already 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 EnableTrimAnalyzer catching
    every reflection call this PR touches).
  • dotnet test (both CommandLineUtils.Tests and
    Hosting.CommandLine.Tests, net8.0 and net10.0) — all 1125 + 18 existing
    tests pass unchanged; no tests were removed or weakened.
  • pwsh -File build.ps1 -ci (the project's own CI script: format
    verification, full build with -warnaserror:CS1591, pack, the samples
    solution build, and the full test run) — succeeds end to end.
  • Manual verification of the actual scenario from the issue:
    dotnet publish docs/samples/trimming/TrimmingSample.csproj -c Release -r linux-x64
    
    publishes cleanly with zero trim warnings (previously this would either
    warn, once analysis was on, or the app would throw at run time), and the
    resulting self-contained trimmed binary runs correctly:
    $ ./docs/samples/trimming/bin/Release/net8.0/linux-x64/publish/TrimmingSample --subject Trimming
    Hello, Trimming!
    $ ./docs/samples/trimming/bin/Release/net8.0/linux-x64/publish/TrimmingSample
    Hello, world!
    $ ./docs/samples/trimming/bin/Release/net8.0/linux-x64/publish/TrimmingSample --help
    Usage: TrimmingSample [options]
    
    Options:
      -s|--subject <SUBJECT>  The subject to greet
                              Default value is: world.
      -?|-h|--help            Show help information.
    
    confirming constructor injection, [Option] binding (including default
    values), OnExecute, and generated help text all survive trimming.

Scope notes

This is scoped to the reflection paths that Execute<TApp> and the attribute
conventions 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-sample currently
suppresses trim/AOT warnings wholesale
(SuppressTrimAnalysisWarnings/EnableTrimAnalyzer=false) with a comment
that "the library still uses reflection internally" — this PR doesn't change
that sample, since fully wiring the generator into Execute<TApp>'s default
path is a larger, separate change; happy to look at that as a follow-up if
useful.

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

codecov Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 80.13%. Comparing base (00c331e) to head (4b83335).

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@akashchamp
akashchamp force-pushed the fix/trim-reflection-annotations-539 branch from 80f59ca to 4b83335 Compare September 25, 2026 18:18

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Trimmed compilation fails with annotations

1 participant