Skip to content

Conversation

@robertmclaws
Copy link
Contributor

Implements #581

  • Created new ImplementationMetadataProvider framework
  • Moved current Reflection-based implementation to ReflectionMetadataProvider
  • Created new AOT-based MetadataProvider
  • Added a bunch of tests for both
  • Added sample AOT console application
  • Source generators are added to the existing package, should work for AOT without code changes

@natemcmaster
Copy link
Owner

Without having a close look yet myself…any hunches on why CI fails?

@robertmclaws
Copy link
Contributor Author

Probably because the build might need to be adjusted for the generators. AOT has additional requirements that might need to be installed for the build.

I'll take a look and drop another PR for the fix. But it should compile locally, and I can post the commands you need to make sure the right VC++ packages are installed on Windows to be able to run the AOT build.

@codecov
Copy link

codecov bot commented Dec 30, 2025

Codecov Report

❌ Patch coverage is 90.96110% with 79 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.93%. Comparing base (812a157) to head (be70945).
⚠️ Report is 14 commits behind head on main.

Files with missing lines Patch % Lines
...LineUtils/Conventions/OptionAttributeConvention.cs 90.18% 9 Missing and 7 partials ⚠️
...neUtils/Conventions/ArgumentAttributeConvention.cs 91.13% 5 Missing and 2 partials ⚠️
src/CommandLineUtils/CommandLineApplication.cs 70.00% 5 Missing and 1 partial ⚠️
...Utils/Conventions/SubcommandAttributeConvention.cs 83.33% 3 Missing and 2 partials ⚠️
...dLineUtils/Conventions/ParentPropertyConvention.cs 50.00% 2 Missing and 2 partials ⚠️
...eUtils/Conventions/SubcommandPropertyConvention.cs 50.00% 2 Missing and 2 partials ⚠️
...eUtils/SourceGeneration/DefaultMetadataResolver.cs 76.47% 2 Missing and 2 partials ⚠️
...tils/SourceGeneration/Metadata/ArgumentMetadata.cs 82.60% 0 Missing and 4 partials ⚠️
...eUtils/SourceGeneration/Metadata/OptionMetadata.cs 84.61% 0 Missing and 4 partials ⚠️
...Utils/SourceGeneration/ReflectionExecuteHandler.cs 87.50% 4 Missing ⚠️
... and 12 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #582      +/-   ##
==========================================
+ Coverage   77.73%   79.93%   +2.19%     
==========================================
  Files         104      120      +16     
  Lines        3342     4017     +675     
  Branches      734      872     +138     
==========================================
+ Hits         2598     3211     +613     
- Misses        581      618      +37     
- Partials      163      188      +25     

☔ View full report in Codecov by Sentry.
📢 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.

@robertmclaws
Copy link
Contributor Author

Fixed the main build issue. Improving code coverage and working on getting the docs fixed now.

robertmclaws and others added 7 commits December 30, 2025 23:47
- DocFX fixes for the generators, which are not a public API
Added tests for:
- CommandMetadataRegistry generic methods (Register<T>, TryGetProvider<T>, HasMetadata<T>)
- ErrorExecuteHandler (constructor, IsAsync, InvokeAsync)
- CommandLineApplicationWithModel (constructor, Model property, IModelAccessor)
- OptionAttributeConventionBase inherited option handling

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Added tests for:
- CommandAttribute.ClusterOptionsWasSet and UnrecognizedArgumentHandlingWasSet
- CommandAttribute.Configure with explicit vs default values
- Convention AOT code paths (generated metadata setters)
- RemainingArgsPropertyConvention with SpecialPropertiesMetadata
- ParentPropertyConvention with SpecialPropertiesMetadata
- SubcommandPropertyConvention with SpecialPropertiesMetadata
- ConventionContext.MetadataProvider and HasGeneratedMetadata
- CommandAttributeConvention with CommandMetadata
- SubcommandAttributeConvention with SubcommandMetadata

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@robertmclaws
Copy link
Contributor Author

  • Meaningful improvements in code coverage
  • Docs compiling and working
  • Tests pass

@natemcmaster I think this is ready for you to review.

Copy link
Owner

@natemcmaster natemcmaster left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking good so far. The fact that I see very few changes to existing unit tests and all tests are now passing CI gives me higher confidence we're on track to release this feature.

I'm only halfway done reading through the code. 11k is a lot and I haven't yet deferred review responsibility to Claude. That said, I wanted to leave you some initial feedback as I'm making progress.

Image

Remaining review steps:

  • Review changes to src/CommandLineUtils/Conventions
  • Review new code in src/CommandLineUtils/SourceGeneration and matching tests
  • Review aot-plan.md for remaining work (like docs or release notes)

@natemcmaster
Copy link
Owner

Claude generated the following feedback:

Code Review for PR #582: Full AOT Support for .NET 8+

Thanks for this substantial contribution! This is a significant architectural change to enable Native AOT compilation. I've reviewed the key code changes and identified a few potential issues worth discussing.


Issue 1: MakeGenericMethod still used in SubcommandAttributeConvention

File: src/CommandLineUtils/Conventions/SubcommandAttributeConvention.cs
Severity: Medium
Issue: The new implementation still uses MakeGenericType() and reflection to create CommandLineApplication<T> instances for subcommands.
Why it matters: The goal of this PR is AOT compatibility, but MakeGenericType() and GetConstructor() with BindingFlags are problematic for AOT scenarios because the runtime cannot know ahead of time which generic instantiations will be needed. When PublishAot=true, this code path may fail at runtime if the required types haven't been preserved.

var genericType = typeof(CommandLineApplication<>).MakeGenericType(subcommandType);
var constructor = genericType.GetConstructor(
    BindingFlags.NonPublic | BindingFlags.Instance,
    null,
    new[] { typeof(CommandLineApplication), typeof(string) },
    null);

Suggested fix: Consider using the CommandLineApplicationWithModel class (which was added in this PR) for subcommand creation in the AOT path, or add the necessary [DynamicallyAccessedMembers] annotations to ensure the types are preserved.


Issue 2: Potential null reference in GeneratedModelFactory when DI fails

File: src/CommandLineUtils.Generators/CommandMetadataGenerator.cs (GenerateModelFactory method)
Severity: Medium
Issue: When a type has only parameterized constructors and DI can't resolve all parameters, the generated code throws InvalidOperationException. However, if _services is null and there's no default constructor, the exception message suggests services are needed but doesn't make it clear what went wrong.

Code:

sb.AppendLine($"{indent}            throw new InvalidOperationException(\"Unable to create instance of {info.ClassName}. No services registered for constructor parameters.\");");

Why it matters: The error message says "No services registered" but the actual problem could be that no IServiceProvider was provided at all. This could lead to confusing debugging experiences.

Suggested fix: Differentiate the error messages:

  • When _services is null: "Unable to create instance of {type}. The type requires constructor parameters but no IServiceProvider was configured."
  • When services don't resolve: "Unable to create instance of {type}. Required service(s) not registered in the IServiceProvider."

Issue 3: Subcommand metadata provider may throw unhelpful exception

File: src/CommandLineUtils.Generators/CommandMetadataGenerator.cs (GenerateSubcommandsProperty)
Severity: Medium
Issue: If a subcommand type doesn't have generated metadata registered, the code throws a generic exception.

sb.AppendLine($"{indent}            MetadataProviderFactory = () => CommandMetadataRegistry.TryGetProvider(typeof({sub.TypeName}), out var p) ? p : throw new InvalidOperationException(\"No metadata provider for \" + typeof({sub.TypeName}))");

Why it matters: In AOT scenarios, if the subcommand class wasn't processed by the source generator (e.g., it's in a different assembly without the generator), the user gets a runtime error with no guidance on how to fix it.

Suggested fix: Consider falling back to DefaultMetadataResolver.Instance.GetProvider() which will use reflection as a fallback, or provide a more actionable error message explaining that the subcommand class needs the [Command] attribute and must be compiled with the source generator.


Issue 4: Async execute handler doesn't await Task before checking return type

File: src/CommandLineUtils.Generators/CommandMetadataGenerator.cs (GenerateExecuteHandler)
Severity: Low
Issue: The generated execute handler code for non-async OnExecute methods wraps the call in an async Task<int> method but doesn't actually await anything, which may generate compiler warnings.

Suggested fix: For sync methods, consider returning Task.FromResult() instead of using async:

public Task<int> InvokeAsync(object model, CommandLineApplication app, CancellationToken cancellationToken)
{
    var typedModel = (ClassName)model;
    typedModel.OnExecute();
    return Task.FromResult(0);
}

Other Observations (not blocking)

  1. Good: The ConcurrentDictionary usage in CommandMetadataRegistry and DefaultMetadataResolver is thread-safe for registration and lookup.

  2. Good: The lazy initialization pattern in ReflectionMetadataProvider is appropriate for deferring reflection costs.

  3. Good: The aot-plan.md file is a nice addition for documenting the architecture, though it might be better placed in a /docs folder rather than the repo root.

  4. Note: The change from IEnumerable<object> to IEnumerable in ArgumentAttributeConvention.cs for handling default collection values is correct and more flexible.


The changes look architecturally sound overall. The dual-mode approach (generated + reflection fallback) is a solid pattern for maintaining backward compatibility while enabling AOT. The issues identified above are primarily about improving error messages and ensuring the AOT path is fully reflection-free.

- Rename CommandInfo to CommandData for naming consistency with other POCOs
- Extract ConstructorParameterData to its own file
- Delete obsolete update_generator.ps1 patching script
- Improve error messages in GenerateModelFactory to distinguish between
  missing IServiceProvider vs unregistered services
- Add fallback to DefaultMetadataResolver for subcommand metadata providers
- Use Task.FromResult for sync OnExecute methods to avoid async warnings
- Add [DynamicallyAccessedMembers] annotation to SubcommandAttributeConvention
  for AOT compatibility

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@robertmclaws
Copy link
Contributor Author

robertmclaws commented Jan 8, 2026

OK, I addressed all of the issues mentioned. The Claude Code review failed, and GitHub Copilot said this about the fix:

The failing job 59771138391 could not obtain an OIDC token because the required GitHub Actions workflow permission was missing. The error message is:

Could not fetch an OIDC token. Did you remember to add id-token: write to your workflow permissions?

Solution: Add the following permissions block to your .github/workflows/claude-code-review.yml file at the top level of the workflow definition:

permissions: id-token: write contents: read

This change will allow the action to request the necessary OIDC token.

@natemcmaster
Copy link
Owner

Yeah, it looks like this is a limitation anthropics/claude-code-action#339. I'm not sure how to configure this to work, so I will just disable it on PRs for now.

@natemcmaster natemcmaster merged commit f0be726 into natemcmaster:main Jan 12, 2026
9 of 10 checks passed
@natemcmaster
Copy link
Owner

Thanks for this contribution @robertmclaws ! I reviewed the changes, with some assistance from claude. It found a couple minor issues, but I think the core pieces are in place. I'd like to make a beta release to nuget and start publicizing to see if we can have more people test it out.

@natemcmaster natemcmaster added this to the 5.0 milestone Jan 12, 2026
natemcmaster added a commit that referenced this pull request Jan 12, 2026
Bug 1 - Value type constructor parameters:
- Changed from 'as {TypeName}' to checking service != null then casting
- Avoids CS0077 compiler error with non-nullable value types
- Generated code now: var service = GetService(...); if (service != null) return new T((Type)service!)

Bug 2 - Nullable array RemainingArguments:
- Changed from string comparison to IArrayTypeSymbol type check
- Fixes issue where 'string[]?' didn't match 'string[]' string comparison
- Now correctly generates ToArray() conversion for all array types

Also added System.Linq using to generated code for ToArray() support.

Fixes #582 review issues
natemcmaster added a commit that referenced this pull request Jan 12, 2026
## Summary

Fixes critical and high-severity bugs in the source generator that were
identified during review of #582.

## Issues Fixed

### Bug 1: Value Type Constructor Parameters (Critical - CS0077)

**Problem:** Generated code used `as` operator with non-nullable value
types:
```csharp
var p0_0 = _services.GetService(typeof(int)) as int;  // CS0077 compiler error
```

**Fix:** Check service object for null, then use direct cast:
```csharp
var service0_0 = _services.GetService(typeof(int));
if (service0_0 != null) return new T((int)service0_0!);
```

**Impact:** Any command with value type constructor parameters (int,
bool, etc.) would fail to compile with CS0077.

### Bug 2: Nullable Array RemainingArguments (High Severity)

**Problem:** Used fragile string comparison for type detection:
```csharp
if (sp.RemainingArgumentsPropertyType == "string[]")  // Fails for "string[]?"
```

**Fix:** Use actual type analysis with `IArrayTypeSymbol`:
```csharp
info.SpecialProperties.RemainingArgumentsIsArray = property.Type is IArrayTypeSymbol;
```

**Impact:** Properties like `string[]?` would use wrong code path,
causing `InvalidCastException` at runtime.

### Additional Fixes

- Added `System.Linq` using to generated code for `ToArray()` support
- Fixed `CommandData` property name to `CommandInfo` to match interface
- Organized Claude planning documents in `.claude/plans/`

## Test Coverage

Added comprehensive tests demonstrating both bugs:

1. **GeneratorBugReproduction/** - Compilation test project that
demonstrates bugs would cause CS0077
2. **GeneratedModelFactoryTests.cs** - Unit tests for value type
handling patterns
3. **SpecialPropertiesMetadataTests.cs** - Tests for array type
conversion logic

All tests now pass ✅
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants