Skip to main content

nu_protocol/engine/
named_flags.rs

1//! Helpers for signature-aware named flag handling (null omit/pass-through and
2//! long-key record flag spreads).
3//!
4//! Used by IR evaluation before dispatch and by const evaluation when building
5//! IR-shaped calls.
6
7use std::sync::Arc;
8
9use crate::{
10    CompareTypes, Flag, Record, ShellError, Signature, Span, Value, engine::Argument,
11    ir::DataSlice, shell_error::generic::GenericError,
12};
13
14/// Whether a named flag's value type accepts `nothing`/`null`.
15#[inline]
16pub fn flag_type_accepts_nothing(flag: &Flag) -> bool {
17    flag.type_accepts_nothing()
18}
19
20pub fn find_signature_flag<'a>(
21    signature: &'a Signature,
22    long: &[u8],
23    short: &[u8],
24) -> Option<&'a Flag> {
25    signature.named.iter().find(|flag| {
26        (!long.is_empty() && flag.long.as_bytes() == long)
27            || (!short.is_empty()
28                && flag.short.is_some_and(|c| {
29                    let mut buf = [0u8; 4];
30                    c.encode_utf8(&mut buf).as_bytes() == short
31                }))
32    })
33}
34
35pub fn data_from_name_and_short(name: &str, short: &str) -> (Arc<[u8]>, DataSlice, DataSlice) {
36    let data: Vec<u8> = name.bytes().chain(short.bytes()).collect();
37    let data: Arc<[u8]> = data.into();
38    // Flag names are parser identifiers — never near u32::MAX.
39    #[allow(clippy::cast_possible_truncation)]
40    let name_len = name.len() as u32;
41    #[allow(clippy::cast_possible_truncation)]
42    let short_len = short.len() as u32;
43    let name = DataSlice {
44        start: 0,
45        len: name_len,
46    };
47    let short = DataSlice {
48        start: name_len,
49        len: short_len,
50    };
51    (data, name, short)
52}
53
54/// Expand a record into named/flag arguments for a call.
55///
56/// - `null` field values: passed through if the flag type accepts `nothing`, otherwise omitted
57/// - switch flags (`--flag` with no value): `true` sets the flag, `false`/`null` omit it
58/// - valued flags: non-null values become `--name value` (type-checked against the signature)
59pub fn expand_flag_record(
60    signature: &Signature,
61    record: Record,
62    spread_span: Span,
63) -> Result<Vec<Argument>, ShellError> {
64    let mut out = Vec::with_capacity(record.len());
65    for (key, val) in record {
66        let Some(flag) = signature.get_long_flag(&key) else {
67            return Err(ShellError::Generic(GenericError::new(
68                format!("Unknown flag `{key}` in spread record"),
69                format!("`{key}` is not a named argument of this command"),
70                spread_span,
71            )));
72        };
73
74        let short = flag
75            .short
76            .map(|c| {
77                let mut buf = [0u8; 4];
78                c.encode_utf8(&mut buf).to_string()
79            })
80            .unwrap_or_default();
81        let (data, name_slice, short_slice) = data_from_name_and_short(&flag.long, &short);
82
83        if flag.arg.is_none() {
84            // Switch: only `true` sets the flag; `false`/`null` omit (like `--flag=false`).
85            match val {
86                Value::Bool { val: true, .. } => {
87                    out.push(Argument::Flag {
88                        data,
89                        name: name_slice,
90                        short: short_slice,
91                        span: spread_span,
92                    });
93                }
94                Value::Bool { val: false, .. } | Value::Nothing { .. } => {}
95                other => {
96                    return Err(ShellError::CantConvert {
97                        to_type: "bool".into(),
98                        from_type: other.get_type().to_string(),
99                        span: other.span(),
100                        help: Some(format!(
101                            "spread field `{key}` is a switch; use true/false or omit/null"
102                        )),
103                    });
104                }
105            }
106        } else if val.is_nothing() && !flag_type_accepts_nothing(&flag) {
107            // Null → omit when the flag type does not accept nothing.
108        } else {
109            if !val.is_nothing()
110                && let Some(shape) = &flag.arg
111            {
112                let expected = shape.to_type();
113                if !val.is_assignable_to(&expected) {
114                    return Err(ShellError::CantConvert {
115                        to_type: expected.to_string(),
116                        from_type: val.get_type().to_string(),
117                        span: val.span(),
118                        help: Some(format!(
119                            "spread field `{key}` does not match the type of `--{key}`"
120                        )),
121                    });
122                }
123            }
124            out.push(Argument::Named {
125                data,
126                name: name_slice,
127                short: short_slice,
128                span: spread_span,
129                val,
130                ast: None,
131            });
132        }
133    }
134    Ok(out)
135}
136
137/// Whether this signature can accept a list rest spread.
138pub fn can_rest_spread(signature: &Signature) -> bool {
139    signature.rest_positional.is_some() || signature.allows_unknown_args
140}
141
142/// Error when a list (or null rest-mode) is spread while required positionals remain unbound.
143pub fn list_spread_before_required_error(spread_span: Span) -> ShellError {
144    ShellError::Generic(GenericError::new(
145        "Cannot spread a list before required positional arguments are provided",
146        "List spreads fill rest arguments. Provide required positionals first, or use a record to spread named flags, e.g. ...{flag: value}",
147        spread_span,
148    ))
149}
150
151/// Apply signature-aware null handling and expand record spreads for engine [`Argument`]s.
152///
153/// List spreads are rejected when the command has no rest parameter (and does not allow
154/// unknown args), so `...$list` cannot be silently dropped on named-only commands.
155pub fn normalize_engine_arguments(
156    signature: &Signature,
157    args: Vec<Argument>,
158) -> Result<Vec<Argument>, ShellError> {
159    let mut expanded = Vec::with_capacity(args.len());
160
161    for arg in args {
162        match arg {
163            Argument::Named {
164                data,
165                name,
166                short,
167                span,
168                val: Value::Nothing { .. },
169                ast,
170            } => {
171                let accepts = find_signature_flag(signature, &data[name], &data[short])
172                    .is_some_and(flag_type_accepts_nothing);
173                if accepts {
174                    expanded.push(Argument::Named {
175                        data,
176                        name,
177                        short,
178                        span,
179                        val: Value::nothing(span),
180                        ast,
181                    });
182                }
183                // else: null → omit
184            }
185            Argument::Spread {
186                vals,
187                span: spread_span,
188                ast,
189            } => match vals {
190                Value::Record { val, .. } => {
191                    expanded.extend(expand_flag_record(
192                        signature,
193                        val.into_owned(),
194                        spread_span,
195                    )?);
196                }
197                Value::List { .. } => {
198                    if !can_rest_spread(signature) {
199                        return Err(ShellError::Generic(GenericError::new(
200                            "Cannot spread a list into this command",
201                            "This command has no ...rest parameter to receive a list spread. Use a record to spread named flags, e.g. ...{flag: value}",
202                            spread_span,
203                        )));
204                    }
205                    expanded.push(Argument::Spread {
206                        vals,
207                        span: spread_span,
208                        ast,
209                    });
210                }
211                Value::Nothing { .. } | Value::Error { .. } => {
212                    expanded.push(Argument::Spread {
213                        vals,
214                        span: spread_span,
215                        ast,
216                    });
217                }
218                other => {
219                    return Err(ShellError::CannotSpreadAsList { span: other.span() });
220                }
221            },
222            other => expanded.push(other),
223        }
224    }
225
226    Ok(expanded)
227}