diff --git a/crates/nu-cmd-dataframe/src/dataframe/eager/drop_duplicates.rs b/crates/nu-cmd-dataframe/src/dataframe/eager/drop_duplicates.rs index 80b1ab61de..b9d796f49f 100644 --- a/crates/nu-cmd-dataframe/src/dataframe/eager/drop_duplicates.rs +++ b/crates/nu-cmd-dataframe/src/dataframe/eager/drop_duplicates.rs @@ -25,7 +25,7 @@ impl Command for DropDuplicates { Signature::build(self.name()) .optional( "subset", - SyntaxShape::Table, + SyntaxShape::Table(vec![]), "subset of columns to drop duplicates", ) .switch("maintain", "maintain order", Some('m')) diff --git a/crates/nu-cmd-dataframe/src/dataframe/eager/drop_nulls.rs b/crates/nu-cmd-dataframe/src/dataframe/eager/drop_nulls.rs index 071db45ca9..4ab7b8485b 100644 --- a/crates/nu-cmd-dataframe/src/dataframe/eager/drop_nulls.rs +++ b/crates/nu-cmd-dataframe/src/dataframe/eager/drop_nulls.rs @@ -24,7 +24,7 @@ impl Command for DropNulls { Signature::build(self.name()) .optional( "subset", - SyntaxShape::Table, + SyntaxShape::Table(vec![]), "subset of columns to drop nulls", ) .input_output_type( diff --git a/crates/nu-cmd-dataframe/src/dataframe/eager/melt.rs b/crates/nu-cmd-dataframe/src/dataframe/eager/melt.rs index 39a908fcfb..95565df4ac 100644 --- a/crates/nu-cmd-dataframe/src/dataframe/eager/melt.rs +++ b/crates/nu-cmd-dataframe/src/dataframe/eager/melt.rs @@ -26,13 +26,13 @@ impl Command for MeltDF { Signature::build(self.name()) .required_named( "columns", - SyntaxShape::Table, + SyntaxShape::Table(vec![]), "column names for melting", Some('c'), ) .required_named( "values", - SyntaxShape::Table, + SyntaxShape::Table(vec![]), "column names used as value columns", Some('v'), ) diff --git a/crates/nu-cmd-dataframe/src/dataframe/eager/summary.rs b/crates/nu-cmd-dataframe/src/dataframe/eager/summary.rs index 9d2e0197f9..7f30ed1b2d 100644 --- a/crates/nu-cmd-dataframe/src/dataframe/eager/summary.rs +++ b/crates/nu-cmd-dataframe/src/dataframe/eager/summary.rs @@ -35,7 +35,7 @@ impl Command for Summary { ) .named( "quantiles", - SyntaxShape::Table, + SyntaxShape::Table(vec![]), "provide optional quantiles", Some('q'), ) diff --git a/crates/nu-cmd-extra/src/extra/bytes/index_of.rs b/crates/nu-cmd-extra/src/extra/bytes/index_of.rs index 078347af32..0090f46112 100644 --- a/crates/nu-cmd-extra/src/extra/bytes/index_of.rs +++ b/crates/nu-cmd-extra/src/extra/bytes/index_of.rs @@ -32,6 +32,9 @@ impl Command for BytesIndexOf { .input_output_types(vec![ (Type::Binary, Type::Int), (Type::Binary, Type::List(Box::new(Type::Int))), + // FIXME: this shouldn't be needed, cell paths should work with the two + // above + (Type::Table(vec![]), Type::Table(vec![])), ]) .required( "pattern", diff --git a/crates/nu-cmd-extra/src/extra/filters/update_cells.rs b/crates/nu-cmd-extra/src/extra/filters/update_cells.rs index 621c972875..f109f7552e 100644 --- a/crates/nu-cmd-extra/src/extra/filters/update_cells.rs +++ b/crates/nu-cmd-extra/src/extra/filters/update_cells.rs @@ -26,7 +26,7 @@ impl Command for UpdateCells { ) .named( "columns", - SyntaxShape::Table, + SyntaxShape::Table(vec![]), "list of columns to update", Some('c'), ) diff --git a/crates/nu-command/src/conversions/into/decimal.rs b/crates/nu-command/src/conversions/into/decimal.rs index 04d7fba80b..11ceb0a902 100644 --- a/crates/nu-command/src/conversions/into/decimal.rs +++ b/crates/nu-command/src/conversions/into/decimal.rs @@ -19,6 +19,7 @@ impl Command for SubCommand { .input_output_types(vec![ (Type::String, Type::Number), (Type::Bool, Type::Number), + (Type::Table(vec![]), Type::Table(vec![])), ]) .rest( "rest", diff --git a/crates/nu-command/src/conversions/into/duration.rs b/crates/nu-command/src/conversions/into/duration.rs index 749e0fb9b8..c840447d49 100644 --- a/crates/nu-command/src/conversions/into/duration.rs +++ b/crates/nu-command/src/conversions/into/duration.rs @@ -23,6 +23,7 @@ impl Command for SubCommand { // TODO: --convert option should be implemented as `format duration` (Type::String, Type::String), (Type::Duration, Type::String), + (Type::Table(vec![]), Type::Table(vec![])), ]) .named( "convert", diff --git a/crates/nu-command/src/path/basename.rs b/crates/nu-command/src/path/basename.rs index 956e61ba91..8c005b1ab0 100644 --- a/crates/nu-command/src/path/basename.rs +++ b/crates/nu-command/src/path/basename.rs @@ -38,7 +38,7 @@ impl Command for SubCommand { ]) .named( "columns", - SyntaxShape::Table, + SyntaxShape::Table(vec![]), "For a record or table input, convert strings in the given columns to their basename", Some('c'), ) diff --git a/crates/nu-command/src/path/dirname.rs b/crates/nu-command/src/path/dirname.rs index 69edccbd7a..323fb77977 100644 --- a/crates/nu-command/src/path/dirname.rs +++ b/crates/nu-command/src/path/dirname.rs @@ -35,7 +35,7 @@ impl Command for SubCommand { .input_output_types(vec![(Type::String, Type::String)]) .named( "columns", - SyntaxShape::Table, + SyntaxShape::Table(vec![]), "For a record or table input, convert strings at the given columns to their dirname", Some('c'), ) diff --git a/crates/nu-command/src/path/exists.rs b/crates/nu-command/src/path/exists.rs index 388c1813af..f09e81fe4f 100644 --- a/crates/nu-command/src/path/exists.rs +++ b/crates/nu-command/src/path/exists.rs @@ -34,7 +34,7 @@ impl Command for SubCommand { .input_output_types(vec![(Type::String, Type::Bool)]) .named( "columns", - SyntaxShape::Table, + SyntaxShape::Table(vec![]), "For a record or table input, check strings at the given columns, and replace with result", Some('c'), ) diff --git a/crates/nu-command/src/path/expand.rs b/crates/nu-command/src/path/expand.rs index 1252d9ee03..1aff543b25 100644 --- a/crates/nu-command/src/path/expand.rs +++ b/crates/nu-command/src/path/expand.rs @@ -43,7 +43,7 @@ impl Command for SubCommand { .switch("no-symlink", "Do not resolve symbolic links", Some('n')) .named( "columns", - SyntaxShape::Table, + SyntaxShape::Table(vec![]), "For a record or table input, expand strings at the given columns", Some('c'), ) diff --git a/crates/nu-command/src/path/join.rs b/crates/nu-command/src/path/join.rs index 371f497912..603a9a0c04 100644 --- a/crates/nu-command/src/path/join.rs +++ b/crates/nu-command/src/path/join.rs @@ -39,7 +39,7 @@ impl Command for SubCommand { ]) .named( "columns", - SyntaxShape::Table, + SyntaxShape::Table(vec![]), "For a record or table input, join strings at the given columns", Some('c'), ) diff --git a/crates/nu-command/src/path/parse.rs b/crates/nu-command/src/path/parse.rs index 2f9f212cad..0eac1b9cec 100644 --- a/crates/nu-command/src/path/parse.rs +++ b/crates/nu-command/src/path/parse.rs @@ -35,7 +35,7 @@ impl Command for SubCommand { .input_output_types(vec![(Type::String, Type::Record(vec![]))]) .named( "columns", - SyntaxShape::Table, + SyntaxShape::Table(vec![]), "For a record or table input, convert strings at the given columns", Some('c'), ) diff --git a/crates/nu-command/src/path/relative_to.rs b/crates/nu-command/src/path/relative_to.rs index b8fdc90fc5..6b7f531cf3 100644 --- a/crates/nu-command/src/path/relative_to.rs +++ b/crates/nu-command/src/path/relative_to.rs @@ -40,7 +40,7 @@ impl Command for SubCommand { ) .named( "columns", - SyntaxShape::Table, + SyntaxShape::Table(vec![]), "For a record or table input, convert strings at the given columns", Some('c'), ) diff --git a/crates/nu-command/src/path/split.rs b/crates/nu-command/src/path/split.rs index 35ca70a64c..a76d8c33e8 100644 --- a/crates/nu-command/src/path/split.rs +++ b/crates/nu-command/src/path/split.rs @@ -32,7 +32,7 @@ impl Command for SubCommand { .input_output_types(vec![(Type::String, Type::List(Box::new(Type::String)))]) .named( "columns", - SyntaxShape::Table, + SyntaxShape::Table(vec![]), "For a record or table input, split strings at the given columns", Some('c'), ) diff --git a/crates/nu-command/src/path/type.rs b/crates/nu-command/src/path/type.rs index cd5db09bb4..7c9c412c4a 100644 --- a/crates/nu-command/src/path/type.rs +++ b/crates/nu-command/src/path/type.rs @@ -32,7 +32,7 @@ impl Command for SubCommand { .input_output_types(vec![(Type::String, Type::String)]) .named( "columns", - SyntaxShape::Table, + SyntaxShape::Table(vec![]), "For a record or table input, check strings at the given columns, and replace with result", Some('c'), ) diff --git a/crates/nu-command/src/strings/str_/contains.rs b/crates/nu-command/src/strings/str_/contains.rs index 284405d332..ef47588fb1 100644 --- a/crates/nu-command/src/strings/str_/contains.rs +++ b/crates/nu-command/src/strings/str_/contains.rs @@ -29,7 +29,10 @@ impl Command for SubCommand { fn signature(&self) -> Signature { Signature::build("str contains") - .input_output_types(vec![(Type::String, Type::Bool)]) + .input_output_types(vec![ + (Type::String, Type::Bool), + (Type::Table(vec![]), Type::Table(vec![])), + ]) .vectorizes_over_list(true) .required("string", SyntaxShape::String, "the substring to find") .rest( diff --git a/crates/nu-command/src/strings/str_/distance.rs b/crates/nu-command/src/strings/str_/distance.rs index 695574dbdb..a6a5510b3b 100644 --- a/crates/nu-command/src/strings/str_/distance.rs +++ b/crates/nu-command/src/strings/str_/distance.rs @@ -28,7 +28,10 @@ impl Command for SubCommand { fn signature(&self) -> Signature { Signature::build("str distance") - .input_output_types(vec![(Type::String, Type::Int)]) + .input_output_types(vec![ + (Type::String, Type::Int), + (Type::Table(vec![]), Type::Table(vec![])), + ]) .required( "compare-string", SyntaxShape::String, diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index bc44b80ca2..0c6e31989f 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -2724,7 +2724,7 @@ pub fn parse_shape_name( _ if bytes.starts_with(b"record") => parse_collection_shape(working_set, bytes, span), b"signature" => SyntaxShape::Signature, b"string" => SyntaxShape::String, - b"table" => SyntaxShape::Table, + _ if bytes.starts_with(b"table") => parse_collection_shape(working_set, bytes, span), b"variable" => SyntaxShape::Variable, b"var-with-opt-type" => SyntaxShape::VarWithOptType, _ => { @@ -2765,14 +2765,24 @@ fn parse_collection_shape( bytes: &[u8], span: Span, ) -> SyntaxShape { - assert!(bytes.starts_with(b"record")); - let name = "record"; - let mk_shape = SyntaxShape::Record; + assert!(bytes.starts_with(b"record") || bytes.starts_with(b"table")); + let is_table = bytes.starts_with(b"table"); + + let name = if is_table { "table" } else { "record" }; + let prefix = (if is_table { "table<" } else { "record<" }).as_bytes(); + let prefix_len = prefix.len(); + let mk_shape = |ty| -> SyntaxShape { + if is_table { + SyntaxShape::Table(ty) + } else { + SyntaxShape::Record(ty) + } + }; if bytes == name.as_bytes() { mk_shape(vec![]) - } else if bytes.starts_with(b"record<") { - let Some(inner_span) = prepare_inner_span(working_set, bytes, span, 7) else { + } else if bytes.starts_with(prefix) { + let Some(inner_span) = prepare_inner_span(working_set, bytes, span, prefix_len) else { return SyntaxShape::Any; }; @@ -3902,122 +3912,173 @@ pub fn parse_list_expression( } } -pub fn parse_table_expression( - working_set: &mut StateWorkingSet, - original_span: Span, -) -> Expression { - let bytes = working_set.get_span_contents(original_span); +fn parse_table_expression(working_set: &mut StateWorkingSet, span: Span) -> Expression { + let bytes = working_set.get_span_contents(span); + let inner_span = { + let start = if bytes.starts_with(b"[") { + span.start + 1 + } else { + span.start + }; - let mut start = original_span.start; - let mut end = original_span.end; + let end = if bytes.ends_with(b"]") { + span.end - 1 + } else { + let end = span.end; + working_set.error(ParseError::Unclosed("]".into(), Span::new(end, end))); + span.end + }; - if bytes.starts_with(b"[") { - start += 1; - } - if bytes.ends_with(b"]") { - end -= 1; - } else { - working_set.error(ParseError::Unclosed("]".into(), Span::new(end, end))); - } - - let inner_span = Span::new(start, end); + Span::new(start, end) + }; let source = working_set.get_span_contents(inner_span); - - let (output, err) = lex(source, start, &[b'\n', b'\r', b','], &[], true); + let (tokens, err) = lex(source, inner_span.start, &[b'\n', b'\r', b','], &[], true); if let Some(err) = err { working_set.error(err); } - let (output, err) = lite_parse(&output); - if let Some(err) = err { - working_set.error(err); - } - - match output.block.len() { - 0 => Expression { - expr: Expr::List(vec![]), - span: original_span, - ty: Type::List(Box::new(Type::Any)), - custom_completion: None, - }, - 1 => { - // List - parse_list_expression(working_set, original_span, &SyntaxShape::Any) + let head = if let Some(first) = tokens.first() { + if working_set.get_span_contents(first.span).starts_with(b"[") { + parse_list_expression(working_set, first.span, &SyntaxShape::Any) + } else { + return parse_list_expression(working_set, span, &SyntaxShape::Any); } - _ => { - match &output.block[0].commands[0] { - LiteElement::Command(_, command) - | LiteElement::Redirection(_, _, command) - | LiteElement::SeparateRedirection { - out: (_, command), .. + } else { + return parse_list_expression(working_set, span, &SyntaxShape::Any); + }; + + if tokens + .get(1) + .filter(|second| second.contents == TokenContents::Semicolon) + .is_none() + { + return parse_list_expression(working_set, span, &SyntaxShape::Any); + }; + + let rest = &tokens[2..]; + if rest.is_empty() { + return parse_list_expression(working_set, span, &SyntaxShape::Any); + } + + let head = { + let Expression { expr: Expr::List(vals), .. } = head else { + unreachable!("head must be a list by now") + }; + + vals + }; + + let errors = working_set.parse_errors.len(); + + let rows = rest + .iter() + .fold(Vec::with_capacity(rest.len()), |mut acc, it| { + use std::cmp::Ordering; + let text = working_set.get_span_contents(it.span).to_vec(); + match text.as_slice() { + b"," => acc, + _ if !&text.starts_with(b"[") => { + let err = ParseError::LabeledErrorWithHelp { + error: String::from("Table item not list"), + label: String::from("not a list"), + span: it.span, + help: String::from("All table items must be lists"), + }; + working_set.error(err); + acc } - | LiteElement::SameTargetRedirection { - cmd: (_, command), .. - } => { - let mut table_headers = vec![]; - - let headers = - parse_list_expression(working_set, command.parts[0], &SyntaxShape::Any); - - if let Expression { - expr: Expr::List(headers), + _ => { + let ls = parse_list_expression(working_set, it.span, &SyntaxShape::Any); + let Expression { + expr: Expr::List(item), + span, .. - } = headers - { - table_headers = headers; + } = ls else { + unreachable!("the item must be a list") + }; + + match item.len().cmp(&head.len()) { + Ordering::Less => { + let err = ParseError::MissingColumns(head.len(), span); + working_set.error(err); + } + Ordering::Greater => { + let span = { + let start = item[head.len()].span.start; + let end = span.end; + Span::new(start, end) + }; + let err = ParseError::ExtraColumns(head.len(), span); + working_set.error(err); + } + Ordering::Equal => {} } - match &output.block[1].commands[0] { - LiteElement::Command(_, command) - | LiteElement::Redirection(_, _, command) - | LiteElement::SeparateRedirection { - out: (_, command), .. - } - | LiteElement::SameTargetRedirection { - cmd: (_, command), .. - } => { - let mut rows = vec![]; - for part in &command.parts { - let values = - parse_list_expression(working_set, *part, &SyntaxShape::Any); - if let Expression { - expr: Expr::List(values), - span, - .. - } = values - { - match values.len().cmp(&table_headers.len()) { - std::cmp::Ordering::Less => working_set.error( - ParseError::MissingColumns(table_headers.len(), span), - ), - std::cmp::Ordering::Equal => {} - std::cmp::Ordering::Greater => { - working_set.error(ParseError::ExtraColumns( - table_headers.len(), - values[table_headers.len()].span, - )) - } - } - - rows.push(values); - } - } - - Expression { - expr: Expr::Table(table_headers, rows), - span: original_span, - ty: Type::Table(vec![]), //FIXME - custom_completion: None, - } - } - } + acc.push(item); + acc } } - } + }); + + let ty = if working_set.parse_errors.len() == errors { + let (ty, errs) = table_type(&head, &rows); + working_set.parse_errors.extend(errs.into_iter()); + ty + } else { + Type::Table(vec![]) + }; + + Expression { + expr: Expr::Table(head, rows), + span, + ty, + custom_completion: None, } } +fn table_type(head: &[Expression], rows: &[Vec]) -> (Type, Vec) { + let mut errors = vec![]; + let mut rows = rows.to_vec(); + let mut mk_ty = || -> Type { + rows.iter_mut() + .map(|row| row.pop().map(|x| x.ty).unwrap_or_default()) + .reduce(|acc, ty| -> Type { + if type_compatible(&acc, &ty) { + ty + } else { + Type::Any + } + }) + .unwrap_or_default() + }; + + let mk_error = |span| ParseError::LabeledErrorWithHelp { + error: "Table column name not string".into(), + label: "must be a string".into(), + help: "Table column names should be able to be converted into strings".into(), + span, + }; + + let mut ty = head + .iter() + .rev() + .map(|expr| { + if let Some(str) = expr.as_string() { + str + } else { + errors.push(mk_error(expr.span)); + String::from("{ column }") + } + }) + .map(|title| (title, mk_ty())) + .collect_vec(); + + ty.reverse(); + + (Type::Table(ty), errors) +} + pub fn parse_block_expression(working_set: &mut StateWorkingSet, span: Span) -> Expression { trace!("parsing: block expression"); @@ -4456,7 +4517,7 @@ pub fn parse_value( b'[' => match shape { SyntaxShape::Any | SyntaxShape::List(_) - | SyntaxShape::Table + | SyntaxShape::Table(_) | SyntaxShape::Signature => {} _ => { working_set.error(ParseError::Expected("non-[] value", span)); @@ -4503,7 +4564,7 @@ pub fn parse_value( Expression::garbage(span) } } - SyntaxShape::Table => { + SyntaxShape::Table(_) => { if bytes.starts_with(b"[") { parse_table_expression(working_set, span) } else { diff --git a/crates/nu-parser/src/type_check.rs b/crates/nu-parser/src/type_check.rs index bef8748510..bb4f4e3a2d 100644 --- a/crates/nu-parser/src/type_check.rs +++ b/crates/nu-parser/src/type_check.rs @@ -32,8 +32,8 @@ pub fn type_compatible(lhs: &Type, rhs: &Type) -> bool { (Type::Closure, Type::Block) => true, (Type::Any, _) => true, (_, Type::Any) => true, - (Type::Record(fields_lhs), Type::Record(fields_rhs)) => { - is_compatible(fields_lhs, fields_rhs) + (Type::Record(lhs), Type::Record(rhs)) | (Type::Table(lhs), Type::Table(rhs)) => { + is_compatible(lhs, rhs) } (lhs, rhs) => lhs == rhs, } diff --git a/crates/nu-protocol/src/syntax_shape.rs b/crates/nu-protocol/src/syntax_shape.rs index 195cd2bfa3..142c0eba29 100644 --- a/crates/nu-protocol/src/syntax_shape.rs +++ b/crates/nu-protocol/src/syntax_shape.rs @@ -108,7 +108,7 @@ pub enum SyntaxShape { String, /// A table is allowed, eg `[[first, second]; [1, 2]]` - Table, + Table(Vec<(String, SyntaxShape)>), /// A variable name, eg `$foo` Variable, @@ -119,6 +119,12 @@ pub enum SyntaxShape { impl SyntaxShape { pub fn to_type(&self) -> Type { + let mk_ty = |tys: &[(String, SyntaxShape)]| { + tys.iter() + .map(|(key, val)| (key.clone(), val.to_type())) + .collect() + }; + match self { SyntaxShape::Any => Type::Any, SyntaxShape::Block => Type::Block, @@ -151,18 +157,12 @@ impl SyntaxShape { SyntaxShape::OneOf(_) => Type::Any, SyntaxShape::Operator => Type::Any, SyntaxShape::Range => Type::Any, - SyntaxShape::Record(entries) => { - let ty = entries - .iter() - .map(|(key, val)| (key.clone(), val.to_type())) - .collect(); - Type::Record(ty) - } + SyntaxShape::Record(entries) => Type::Record(mk_ty(entries)), SyntaxShape::RowCondition => Type::Bool, SyntaxShape::Boolean => Type::Bool, SyntaxShape::Signature => Type::Signature, SyntaxShape::String => Type::String, - SyntaxShape::Table => Type::Table(vec![]), // FIXME: What role should columns play in the Table type? + SyntaxShape::Table(columns) => Type::Table(mk_ty(columns)), SyntaxShape::VarWithOptType => Type::Any, SyntaxShape::Variable => Type::Any, } @@ -171,6 +171,13 @@ impl SyntaxShape { impl Display for SyntaxShape { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mk_fmt = |tys: &[(String, SyntaxShape)]| -> String { + tys.iter() + .map(|(x, y)| format!("{x}: {y}")) + .collect::>() + .join(", ") + }; + match self { SyntaxShape::Keyword(kw, shape) => { write!(f, "\"{}\" {}", String::from_utf8_lossy(kw), shape) @@ -198,21 +205,19 @@ impl Display for SyntaxShape { } } SyntaxShape::Binary => write!(f, "binary"), - SyntaxShape::Table => write!(f, "table"), SyntaxShape::List(x) => write!(f, "list<{x}>"), + SyntaxShape::Table(columns) => { + if columns.is_empty() { + write!(f, "table") + } else { + write!(f, "table<{}>", mk_fmt(columns)) + } + } SyntaxShape::Record(entries) => { if entries.is_empty() { write!(f, "record") } else { - write!( - f, - "record<{}>", - entries - .iter() - .map(|(x, y)| format!("{x}: {y}")) - .collect::>() - .join(", "), - ) + write!(f, "record<{}>", mk_fmt(entries)) } } SyntaxShape::Filesize => write!(f, "filesize"), diff --git a/crates/nu-protocol/src/ty.rs b/crates/nu-protocol/src/ty.rs index 88924b505c..a166b611b3 100644 --- a/crates/nu-protocol/src/ty.rs +++ b/crates/nu-protocol/src/ty.rs @@ -81,6 +81,12 @@ impl Type { } pub fn to_shape(&self) -> SyntaxShape { + let mk_shape = |tys: &[(String, Type)]| { + tys.iter() + .map(|(key, val)| (key.clone(), val.to_shape())) + .collect() + }; + match self { Type::Int => SyntaxShape::Int, Type::Float => SyntaxShape::Number, @@ -96,14 +102,8 @@ impl Type { Type::List(x) => SyntaxShape::List(Box::new(x.to_shape())), Type::Number => SyntaxShape::Number, Type::Nothing => SyntaxShape::Nothing, - Type::Record(entries) => { - let entries = entries - .iter() - .map(|(key, val)| (key.clone(), val.to_shape())) - .collect(); - SyntaxShape::Record(entries) - } - Type::Table(_) => SyntaxShape::Table, + Type::Record(entries) => SyntaxShape::Record(mk_shape(entries)), + Type::Table(columns) => SyntaxShape::Table(mk_shape(columns)), Type::ListStream => SyntaxShape::List(Box::new(SyntaxShape::Any)), Type::Any => SyntaxShape::Any, Type::Error => SyntaxShape::Any, diff --git a/crates/nu_plugin_query/src/nu/mod.rs b/crates/nu_plugin_query/src/nu/mod.rs index ac617fb5b2..3dc519dbef 100644 --- a/crates/nu_plugin_query/src/nu/mod.rs +++ b/crates/nu_plugin_query/src/nu/mod.rs @@ -32,7 +32,7 @@ impl Plugin for Query { ) .named( "as-table", - SyntaxShape::Table, + SyntaxShape::Table(vec![]), "find table based on column header list", Some('t'), ) diff --git a/src/tests/test_signatures.rs b/src/tests/test_signatures.rs index d21e2afa5e..1ca395c90f 100644 --- a/src/tests/test_signatures.rs +++ b/src/tests/test_signatures.rs @@ -262,3 +262,116 @@ fn record_annotations_with_extra_characters() -> TestResult { let expected = "Extra characters in the parameter name"; fail_test(input, expected) } + +#[test] +fn table_annotations_none() -> TestResult { + let input = "def run [t: table] { $t }; run [[]; []] | describe"; + let expected = "table"; + run_test(input, expected) +} + +#[test] +fn table_annotations() -> TestResult { + let input = "def run [t: table] { $t }; run [[age]; [3]] | describe"; + let expected = "table"; + run_test(input, expected) +} + +#[test] +fn table_annotations_two_types() -> TestResult { + let input = "\ +def run [t: table] { $t }; +run [[name, age]; [nushell, 3]] | describe"; + let expected = "table"; + run_test(input, expected) +} + +#[test] +fn table_annotations_two_types_comma_sep() -> TestResult { + let input = "\ +def run [t: table] { $t }; +run [[name, age]; [nushell, 3]] | describe"; + let expected = "table"; + run_test(input, expected) +} + +#[test] +fn table_annotations_key_with_no_type() -> TestResult { + let input = "def run [t: table] { $t }; run [[name]; [nushell]] | describe"; + let expected = "table"; + run_test(input, expected) +} + +#[test] +fn table_annotations_two_types_one_with_no_type() -> TestResult { + let input = "\ +def run [t: table] { $t }; +run [[name, age]; [nushell, 3]] | describe"; + let expected = "table"; + run_test(input, expected) +} + +#[test] +fn table_annotations_two_types_both_with_no_types() -> TestResult { + let input = "\ +def run [t: table] { $t }; +run [[name, age]; [nushell, 3]] | describe"; + let expected = "table"; + run_test(input, expected) +} + +#[test] +fn table_annotations_type_inference_1() -> TestResult { + let input = "def run [t: table] { $t }; run [[age]; [2wk]] | describe"; + let expected = "table"; + run_test(input, expected) +} + +#[test] +fn table_annotations_type_inference_2() -> TestResult { + let input = "def run [t: table] { $t }; run [[size]; [2mb]] | describe"; + let expected = "table"; + run_test(input, expected) +} + +#[test] +fn table_annotations_not_terminated() -> TestResult { + let input = "def run [t: table TestResult { + let input = "def run [t: table] { $t }"; + let expected = "expected closing >"; + fail_test(input, expected) +} + +#[test] +fn table_annotations_no_type_after_colon() -> TestResult { + let input = "def run [t: table] { $t }"; + let expected = "type after colon"; + fail_test(input, expected) +} + +#[test] +fn table_annotations_type_mismatch_column() -> TestResult { + let input = "def run [t: table] { $t }; run [[nme]; [nushell]]"; + let expected = "expected table, found table"; + fail_test(input, expected) +} + +#[test] +fn table_annotations_type_mismatch_shape() -> TestResult { + let input = "def run [t: table] { $t }; run [[age]; [2wk]]"; + let expected = "expected table, found table"; + fail_test(input, expected) +} + +#[test] +fn table_annotations_with_extra_characters() -> TestResult { + let input = "def run [t: tableextra] {$t | length}; run [[int]; [8]]"; + let expected = "Extra characters in the parameter name"; + fail_test(input, expected) +}