feat(derive): Add #[command(defer)] for lazy subcommand initialization#6256
feat(derive): Add #[command(defer)] for lazy subcommand initialization#6256r-near wants to merge 5 commits into
Conversation
6477449 to
63f512f
Compare
There was a problem hiding this comment.
Pull request overview
This PR adds support in clap_derive for a new #[command(defer = ...)] attribute to enable lazy initialization of subcommand arguments (via Command::defer), aligning derive behavior with the existing builder API and improving startup time for CLIs with large subcommand trees.
Changes:
- Add
defertracking to deriveItemand parse#[command(defer = true/false)]. - Generate deferred subcommand augmentation for derive-generated subcommands, splitting eager metadata from deferred arg/subcommand materialization.
- Add/adjust derive tests to validate deferred behavior and ensure help-related introspection calls
Command::build()when needed.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/derive/help.rs | Updates tests to call cmd.build() before subcommand introspection and reformats an assertion. |
| tests/derive/defer.rs | Adds new integration tests covering deferred subcommand initialization (including nested subcommands and version-dependent defaults). |
| clap_derive/src/item.rs | Introduces Item::defer state with a default tied to unstable-v5, parses the new magic attribute, and exposes a getter. |
| clap_derive/src/derives/subcommand.rs | Wraps subcommand augmentation in Command::defer(...) when enabled and avoids duplicating top-level method generation in the deferred path. |
| clap_derive/src/derives/args.rs | Adds skip_top_level_methods plumbing to support eager metadata + deferred args generation without duplication. |
| clap_derive/src/attr.rs | Adds lit_bool_or_abort and registers defer as a magic attribute name. |
Comments suppressed due to low confidence (1)
clap_derive/src/derives/subcommand.rs:343
- For named variants,
args::gen_augment(...)already prependsparent_item.deprecations()(seederives/args.rs), but this code also injectsitem.deprecations()into the surroundingsubcommand({ ... })block. This can lead to duplicated deprecation warnings/work for the same item. Consider suppressing one of these (e.g., add a flag toargs::gen_augmentto skip deprecations when the caller handles them, or only emit#deprecationshere for the non-Namedbranches).
let deprecations = if !override_required {
item.deprecations()
} else {
quote!()
};
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
1a5e579 to
c16a824
Compare
| let initial_app_methods = item.initial_top_level_methods(); | ||
| let final_app_methods = item.final_top_level_methods(); |
There was a problem hiding this comment.
The intention would be that we would do this consistently across all derives
There was a problem hiding this comment.
Ok both callers now apply initial/final uniformly. Added here 560fef5 (this PR)
|
Thanks! This is almost there, just some clean up. |
c16a824 to
43a6e00
Compare
| let sub_augment = if parent_item.defer() { | ||
| quote! { | ||
| #subcommand_var.defer(|#subcommand_var| { #sub_augment }) | ||
| } | ||
| } else { | ||
| sub_augment | ||
| }; |
There was a problem hiding this comment.
We don't seem to do this for Args. We're missing a test case for this.
Looking back at #6225, I see deferring of arguments discussed at #6225 (comment)
This is causing me to look back at #4959 where new trait methods are suggested and I'm wondering why I suggested those originally and if there is some missing functionality, e.g. around flatten.
There was a problem hiding this comment.
Added a test case for this.
So for the current scope, I think flatten is covered under deferred subcommands, right?
The extra trait-method ideas from #4959 seem like broader defer semantics. IDK if you wanna add that into this PR?
Move the application of initial_top_level_methods and final_top_level_methods out of gen_augment and into its callers in both args.rs and subcommand.rs. This gives callers control over when and how metadata (about, version, etc.) is applied, separately from args and groups. Part of clap-rs#4959
Restructure Kind::Command branches in subcommand.rs so that all three field variants (Named, Unit, Unnamed) return a (sub_augment, initial_app_methods, final_from_attrs) tuple, with the surrounding code applying them uniformly. Part of clap-rs#4959
Add tests showing the current eager initialization behavior. The subsequent feature commit will modify these to use #[command(defer)] and update assertions to reflect deferred behavior. Tests cover: - Subcommand with named fields - Nested subcommands with Args structs - Flattened Args in subcommands - Enum variants with inline args - Default behavior is eager Part of clap-rs#4959
find_subcommand inspects nested subcommands that may be deferred once unstable-v5 defaults defer to true. Calling build() first ensures the subcommands are materialized, making this test forward-compatible. Part of clap-rs#4959
Add support for deferred subcommand initialization in the derive API via #[command(defer = true)]. When enabled, the derive macro generates code that uses Command::defer() to lazily initialize subcommand args, while keeping metadata (about, version, etc.) eagerly applied so that --help displays subcommand descriptions without needing to build. The attribute defaults to false in clap v4 and true when the unstable-v5 feature is enabled. This can dramatically improve startup time for CLIs with many deeply nested subcommands (e.g. 4-6 seconds down to ~12ms for a CLI with 330 subcommands). Part of clap-rs#4959
4349dee to
531fdd5
Compare
|
@epage would you mind taking a look when you get a chance? |
|
Please sir, may I have a review? @epage |
Summary
Adds
#[command(defer = true)]to the derive API, enabling lazy initialization of subcommand arguments viaCommand::defer(). This closes the gap between the builder API (which has hadCommand::defersince #4792) and the derive API..about(),.version(), etc.) is applied eagerly, so--helpdisplays subcommand descriptions without needingbuild()Command::defer()and only materialized when neededfalsein v4,trueunderunstable-v5Motivation
CLIs with many deeply nested subcommands pay a steep startup cost because clap eagerly initializes every subcommand and all their args — even when only one is invoked. For a CLI with ~330 subcommands, this was measured at 4–6 seconds of startup time, reduced to ~12ms with deferred initialization.
See #4959 for the original feature request.
Prior art
This picks up the work from #6225 by @frol, restructured to follow the project's contribution guidelines. Key differences from the original PR:
gen_augmentaccepts askip_top_level_methodsparameter so metadata and args are each generated exactly once (no duplication)Example
Partially addresses #4959