feat: null values can be spread as if they are empty lists or records. (#16399)

# Description
Spread operator `...` treats `null` values as empty collections,
, whichever of list or record is appropriate.

# User-Facing Changes
`null` values can be used with the spread operator (`...`)
This commit is contained in:
Bahex
2025-08-11 23:47:31 +03:00
committed by GitHub
parent a4711af952
commit 5478bdff0e
5 changed files with 50 additions and 18 deletions

View File

@ -612,12 +612,13 @@ fn eval_instruction<D: DebugContext>(
let items = ctx.collect_reg(*items, *span)?;
let list_span = list_value.span();
let items_span = items.span();
let items = match items {
Value::List { vals, .. } => vals,
Value::Nothing { .. } => Vec::new(),
_ => return Err(ShellError::CannotSpreadAsList { span: items_span }),
};
let mut list = list_value.into_list()?;
list.extend(
items
.into_list()
.map_err(|_| ShellError::CannotSpreadAsList { span: items_span })?,
);
list.extend(items);
ctx.put_reg(*src_dst, Value::list(list, list_span).into_pipeline_data());
Ok(Continue)
}
@ -649,11 +650,13 @@ fn eval_instruction<D: DebugContext>(
let record_span = record_value.span();
let items_span = items.span();
let mut record = record_value.into_record()?;
let items = match items {
Value::Record { val, .. } => val.into_owned(),
Value::Nothing { .. } => Record::new(),
_ => return Err(ShellError::CannotSpreadAsRecord { span: items_span }),
};
// Not using .extend() here because it doesn't handle duplicates
for (key, val) in items
.into_record()
.map_err(|_| ShellError::CannotSpreadAsRecord { span: items_span })?
{
for (key, val) in items {
if let Some(first_value) = record.insert(&key, val) {
return Err(ShellError::ColumnDefinedTwice {
col_name: key,
@ -1191,19 +1194,19 @@ fn gather_arguments(
vals,
span: spread_span,
..
} => {
if let Value::List { vals, .. } = vals {
} => match vals {
Value::List { vals, .. } => {
rest.extend(vals);
// Rest variable should span the spread syntax, not the list values
rest_span = Some(rest_span.map_or(spread_span, |s| s.append(spread_span)));
// All further positional args should go to spread
always_spread = true;
} else if let Value::Error { error, .. } = vals {
return Err(*error);
} else {
return Err(ShellError::CannotSpreadAsList { span: vals.span() });
}
Value::Nothing { .. } => {
rest_span = Some(rest_span.map_or(spread_span, |s| s.append(spread_span)));
always_spread = true;
}
Value::Error { error, .. } => return Err(*error),
_ => return Err(ShellError::CannotSpreadAsList { span: vals.span() }),
},
Argument::Flag {
data,
name,

View File

@ -316,6 +316,7 @@ impl Call {
if spread {
match result {
Value::List { mut vals, .. } => output.append(&mut vals),
Value::Nothing { .. } => (),
_ => return Err(ShellError::CannotSpreadAsList { span: expr.span }),
}
} else {

View File

@ -78,6 +78,7 @@ pub trait Eval {
ListItem::Item(expr) => output.push(Self::eval::<D>(state, mut_state, expr)?),
ListItem::Spread(_, expr) => match Self::eval::<D>(state, mut_state, expr)? {
Value::List { vals, .. } => output.extend(vals),
Value::Nothing { .. } => (),
_ => return Err(ShellError::CannotSpreadAsList { span: expr_span }),
},
}

View File

@ -187,6 +187,7 @@ impl Call {
if spread {
match rest_val {
Value::List { vals, .. } => acc.extend(vals.iter().cloned()),
Value::Nothing { .. } => (),
Value::Error { error, .. } => return Err(ShellError::clone(error)),
_ => {
return Err(ShellError::CannotSpreadAsList {

View File

@ -200,3 +200,29 @@ fn respect_shape() -> TestResult {
run_test(r#"def "...$foo" [] {2}; do { ...$foo }"#, "2").unwrap();
run_test(r#"match "...$foo" { ...$foo => 5 }"#, "5")
}
#[test]
fn spread_null() -> TestResult {
// Spread in list
run_test(r#"[1, 2, ...(null)] | to nuon --raw"#, r#"[1,2]"#)?;
// Spread in record
run_test(r#"{a: 1, b: 2, ...(null)} | to nuon --raw"#, r#"{a:1,b:2}"#)?;
// Spread to built-in command's ...rest
run_test(r#"echo 1 2 ...(null) | to nuon --raw"#, r#"[1,2]"#)?;
// Spread to custom command's ...rest
run_test(
r#"
def foo [...rest] { $rest }
foo ...(null) 1 2 ...(null) 3 | to nuon --raw
"#,
r#"[1,2,3]"#,
)?;
// Spread to external command's arguments
assert_eq!(nu!(r#"nu --testbin cococo 1 ...(null) 2"#).out, "1 2");
Ok(())
}