Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -593,12 +593,12 @@ feature list is not an exhaustive audit.
computed version pairs with `version_spec`, and a typed default pairs with
`default`; the expression drives runtime behavior while the explicit literal
keeps emitted KDL deterministic and portable.
- [ ] **One Args type used by more than one command.** tak's `push` and `init`
have the same `--remote` shape. The derive rejects two variants wrapping
one `Args` type because collection is attached to the declaring struct,
forcing two identical structs. `flatten` solves shared fields within a
command but not a whole command body shared under two names. Either support
this directly or document the duplication as an architectural constraint.
- [x] **One Args type used by more than one command.** Static tables belong to
the Args type and command routing belongs to each mounting enum variant,
so one declaration can back multiple commands without duplicate symbols
or cross-command binding. Facade coverage mounts one `SharedArgs` under
two commands, parses both routes, and verifies each emitted command owns
exactly one copy of the shared flag metadata.
- [x] **Inline struct-style subcommand variants.** aube and hk use clap enums
whose variants declare fields directly. usage requires every non-bare
variant to wrap one dedicated Args struct, turning a mechanical migration
Expand Down Expand Up @@ -1144,10 +1144,11 @@ repeated here.
fnox's test helpers in the rewrite experiment. `parse_from` remains the
allocation-free words-only primitive; `parse_from_argv` is the explicit
clap-shaped, argv0-taking variant and preserves multicall selection.
- [ ] **Shared `Args` under multiple commands need wrapper types** in the
derive, where clap lets one struct serve several parents directly.
flatten covers the mise `ConfigLs` shape; the fnox shape is the same
struct as a full command body in two places.
- [x] **Shared `Args` under multiple commands do not need wrapper types.** Keys
are checked per command because that is their routing scope, while each
subcommand variant selects the shared `CommandArgs` table independently.
The same facade regression covers the fnox full-command shape; mise's
flattened `ConfigLs` reuse remains covered by the fleet shadow.
- [ ] **Relationships across a flatten boundary.** A flag in the parent
conflicting with a flag in the flattened group has no spelling; hk and
aube both hit it and enforce post-bind by hand.
Expand Down
118 changes: 81 additions & 37 deletions argv/src/complete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -597,7 +597,7 @@ fn declared_files_at_cursor(
{
return None;
}
let meta = crate::help::find(spec, position.cmd).and_then(|(_, chain)| chain.last().copied());
let meta = metadata_chain_on_route(spec, position).and_then(|chain| chain.last().copied());
let at_cursor = if restarted(meta, split) {
meta.and_then(|m| m.args.first()).map(|m| m.arg)
} else {
Expand Down Expand Up @@ -641,7 +641,7 @@ fn declared_files_at_cursor(
/// for a word nothing is known about, and the `run=` completions a spec can declare.
pub fn complete<'a>(spec: &'a Spec<'a>, split: &Split) -> Completions<'a> {
let position = walk(spec.root.cmd, split.argv());
let meta = crate::help::find(spec, position.cmd).and_then(|(_, chain)| chain.last().copied());
let meta = metadata_chain_on_route(spec, &position).and_then(|chain| chain.last().copied());
let token = split.prefix.as_str();
let candidates = candidates(spec, split);

Expand Down Expand Up @@ -807,7 +807,7 @@ fn overlay_for_name<'o>(
{
return Some(overlay);
}
let (_, chain) = crate::help::find(spec, position.cmd)?;
let chain = metadata_chain_on_route(spec, position)?;
let owner = chain.last()?;
let path: Vec<&str> = chain.iter().skip(1).map(|meta| meta.cmd.name).collect();
overlays.iter().rev().find(|overlay| {
Expand All @@ -828,7 +828,7 @@ fn overlay_at_cursor<'o>(
{
return None;
}
let meta = crate::help::find(spec, position.cmd).and_then(|(_, chain)| chain.last().copied());
let meta = metadata_chain_on_route(spec, position).and_then(|chain| chain.last().copied());
let target = if restarted(meta, split) {
meta.and_then(|owner| {
owner.args.first().map(|field| {
Expand All @@ -840,12 +840,12 @@ fn overlay_at_cursor<'o>(
})
})
} else if let Some(flag) = position.awaiting_value {
flag_meta_owner(spec.root, flag)
flag_meta_owner_on_route(spec, position, flag)
.map(|(owner, field)| (owner, field.value_name.unwrap_or(field.flag.name), false))
} else {
position
.next_arg
.and_then(|arg| arg_meta_owner(spec.root, arg))
.and_then(|arg| arg_meta_owner_on_route(spec, position, arg))
.map(|(owner, field)| {
(
owner,
Expand All @@ -867,13 +867,65 @@ fn overlay_at_cursor<'o>(
if needs_separator && !position.separator_seen {
return None;
}
let (_, chain) = crate::help::find(spec, owner.cmd)?;
let path: Vec<&str> = chain.iter().skip(1).map(|meta| meta.cmd.name).collect();
let chain = metadata_chain_on_route(spec, position)?;
let path: Vec<&str> = if let Some(owner_at) = chain
.iter()
.position(|candidate| core::ptr::eq(*candidate, owner))
{
chain[..=owner_at]
.iter()
.skip(1)
.map(|meta| meta.cmd.name)
.collect()
} else if core::ptr::eq(position.cmd, spec.root.cmd)
&& spec
.root
.subcommands
.iter()
.any(|candidate| core::ptr::eq(*candidate, owner))
{
// A default subcommand can supply the root cursor's first argument without the
// parser descending into it. Its metadata identity is already unambiguous here;
// preserve that implied route instead of falling back to a tree-wide pointer search.
vec![owner.cmd.name]
} else {
return None;
};
overlays.iter().rev().find(|overlay| {
overlay.value.eq_ignore_ascii_case(value) && overlay.command.matches(owner, &path)
})
}

fn flag_meta_owner_on_route<'a>(
spec: &'a Spec<'a>,
position: &Position<'_>,
flag: &Flag<'_>,
) -> Option<(&'a CommandMeta<'a>, &'a FlagMeta<'a>)> {
let chain = metadata_chain_on_route(spec, position)?;
chain.iter().rev().find_map(|owner| {
owner
.flags
.iter()
.find(|field| core::ptr::eq(field.flag, flag))
.map(|field| (*owner, field))
})
}

fn arg_meta_owner_on_route<'a>(
spec: &'a Spec<'a>,
position: &Position<'_>,
arg: &Arg<'_>,
) -> Option<(&'a CommandMeta<'a>, &'a ArgMeta<'a>)> {
let chain = metadata_chain_on_route(spec, position)?;
chain.iter().rev().find_map(|owner| {
owner
.args
.iter()
.find(|field| core::ptr::eq(field.arg, arg))
.map(|field| (*owner, field))
})
}
Comment thread
jdx marked this conversation as resolved.

fn default_subcommand_arg<'a>(
spec: &'a Spec<'a>,
split: &Split,
Expand All @@ -890,40 +942,32 @@ fn default_subcommand_arg<'a>(
.and_then(|sub| sub.args.first().map(|field| (sub, field)))
}

fn flag_meta_owner<'a>(
meta: &'a CommandMeta<'a>,
flag: &Flag<'_>,
) -> Option<(&'a CommandMeta<'a>, &'a FlagMeta<'a>)> {
meta.flags
.iter()
.find(|field| core::ptr::eq(field.flag, flag))
.map(|field| (meta, field))
.or_else(|| {
meta.subcommands
.iter()
.find_map(|sub| flag_meta_owner(sub, flag))
})
}

fn arg_meta_owner<'a>(
meta: &'a CommandMeta<'a>,
arg: &Arg<'_>,
) -> Option<(&'a CommandMeta<'a>, &'a ArgMeta<'a>)> {
meta.args
.iter()
.find(|field| core::ptr::eq(field.arg, arg))
.map(|field| (meta, field))
.or_else(|| {
meta.subcommands
.iter()
.find_map(|sub| arg_meta_owner(sub, arg))
})
/// The metadata route selected by the parser, preserving parent identity even when two
/// wrappers reuse the same nested command tables.
fn metadata_chain_on_route<'a>(
spec: &'a Spec<'a>,
position: &Position<'_>,
) -> Option<Vec<&'a CommandMeta<'a>>> {
if position.path.is_empty() {
return crate::help::find(spec, position.cmd).map(|(_, chain)| chain);
}
let mut chain = vec![spec.root];
let mut current = spec.root;
for (command, _) in position.path.iter().skip(1) {
current = current
.subcommands
.iter()
.copied()
.find(|meta| core::ptr::eq(meta.cmd, *command))?;
chain.push(current);
}
Some(chain)
}

/// Just the candidates this CLI knows about, without the question of paths.
pub fn candidates<'a>(spec: &'a Spec<'a>, split: &Split) -> Vec<Candidate<'a>> {
let position = walk(spec.root.cmd, split.argv());
let meta = crate::help::find(spec, position.cmd).and_then(|(_, chain)| chain.last().copied());
let meta = metadata_chain_on_route(spec, &position).and_then(|chain| chain.last().copied());
let token = split.prefix.as_str();

let mut out = if position.flags_possible && token == "-" {
Expand Down
31 changes: 0 additions & 31 deletions derive/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3200,37 +3200,6 @@ impl Subcommands {
}
}

// Two variants cannot wrap the same struct. A command's values are collected
// into the struct that declares them, and its fields' keys come from that
// struct — so two commands sharing one would collect into whichever was
// reached first, and choosing between them would be a coin toss. Rejected
Comment thread
jdx marked this conversation as resolved.
// rather than silently misbound.
//
// Compared as whole paths: `type_name` renders only the last segment, so
// `add::Op` and `remove::Op` looked identical and two perfectly good commands
// were refused.
let mut types: Vec<(String, Span)> = Vec::new();
for variant in &variants {
if variant.external {
continue;
}
let rendered = quote::ToTokens::to_token_stream(&variant.ty)
.to_string()
.replace(' ', "");
if let Some((_, first)) = types.iter().find(|(t, _)| *t == rendered) {
return Err(dup(
variant.ty.span(),
*first,
&format!(
"two variants both wrap `{rendered}`, and a command collects \
into the struct that declares it — so give each command its \
own struct, even if the fields are identical"
),
));
}
types.push((rendered, variant.ty.span()));
}

Ok(Subcommands {
ident: input.ident.clone(),
variants,
Expand Down
Loading