Input output checking (#9680)

# Description

This PR tights input/output type-checking a bit more. There are a lot of
commands that don't have correct input/output types, so part of the
effort is updating them.

This PR now contains updates to commands that had wrong input/output
signatures. It doesn't add examples for these new signatures, but that
can be follow-up work.

# User-Facing Changes

BREAKING CHANGE BREAKING CHANGE

This work enforces many more checks on pipeline type correctness than
previous nushell versions. This strictness may uncover incompatibilities
in existing scripts or shortcomings in the type information for internal
commands.

# Tests + Formatting
<!--
Don't forget to add tests that cover your changes.

Make sure you've run and fixed any issues with these commands:

- `cargo fmt --all -- --check` to check standard code formatting (`cargo
fmt --all` applies these changes)
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used -A
clippy::needless_collect -A clippy::result_large_err` to check that
you're using the standard code style
- `cargo test --workspace` to check that all tests pass
- `cargo run -- -c "use std testing; testing run-tests --path
crates/nu-std"` to run the tests for the standard library

> **Note**
> from `nushell` you can also use the `toolkit` as follows
> ```bash
> use toolkit.nu # or use an `env_change` hook to activate it
automatically
> toolkit check pr
> ```
-->

# After Submitting
<!-- If your PR had any user-facing changes, update [the
documentation](https://github.com/nushell/nushell.github.io) after the
PR is merged, if necessary. This will help us keep the docs up to date.
-->
This commit is contained in:
JT 2023-07-14 15:20:35 +12:00 committed by GitHub
parent e66139e6bb
commit 786ba3bf91
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
89 changed files with 480 additions and 106 deletions

View file

@ -15,7 +15,13 @@ impl Command for BitsAnd {
fn signature(&self) -> Signature {
Signature::build("bits and")
.input_output_types(vec![(Type::Int, Type::Int)])
.input_output_types(vec![
(Type::Int, Type::Int),
(
Type::List(Box::new(Type::Int)),
Type::List(Box::new(Type::Int)),
),
])
.vectorizes_over_list(true)
.required(
"target",

View file

@ -16,7 +16,13 @@ impl Command for BitsNot {
fn signature(&self) -> Signature {
Signature::build("bits not")
.input_output_types(vec![(Type::Int, Type::Int)])
.input_output_types(vec![
(Type::Int, Type::Int),
(
Type::List(Box::new(Type::Int)),
Type::List(Box::new(Type::Int)),
),
])
.vectorizes_over_list(true)
.switch(
"signed",

View file

@ -15,7 +15,13 @@ impl Command for BitsOr {
fn signature(&self) -> Signature {
Signature::build("bits or")
.input_output_types(vec![(Type::Int, Type::Int)])
.input_output_types(vec![
(Type::Int, Type::Int),
(
Type::List(Box::new(Type::Int)),
Type::List(Box::new(Type::Int)),
),
])
.vectorizes_over_list(true)
.required(
"target",

View file

@ -18,7 +18,13 @@ impl Command for BitsRol {
fn signature(&self) -> Signature {
Signature::build("bits rol")
.input_output_types(vec![(Type::Int, Type::Int)])
.input_output_types(vec![
(Type::Int, Type::Int),
(
Type::List(Box::new(Type::Int)),
Type::List(Box::new(Type::Int)),
),
])
.vectorizes_over_list(true)
.required("bits", SyntaxShape::Int, "number of bits to rotate left")
.switch(

View file

@ -18,7 +18,13 @@ impl Command for BitsRor {
fn signature(&self) -> Signature {
Signature::build("bits ror")
.input_output_types(vec![(Type::Int, Type::Int)])
.input_output_types(vec![
(Type::Int, Type::Int),
(
Type::List(Box::new(Type::Int)),
Type::List(Box::new(Type::Int)),
),
])
.vectorizes_over_list(true)
.required("bits", SyntaxShape::Int, "number of bits to rotate right")
.switch(

View file

@ -18,7 +18,13 @@ impl Command for BitsShl {
fn signature(&self) -> Signature {
Signature::build("bits shl")
.input_output_types(vec![(Type::Int, Type::Int)])
.input_output_types(vec![
(Type::Int, Type::Int),
(
Type::List(Box::new(Type::Int)),
Type::List(Box::new(Type::Int)),
),
])
.vectorizes_over_list(true)
.required("bits", SyntaxShape::Int, "number of bits to shift left")
.switch(

View file

@ -18,7 +18,13 @@ impl Command for BitsShr {
fn signature(&self) -> Signature {
Signature::build("bits shr")
.input_output_types(vec![(Type::Int, Type::Int)])
.input_output_types(vec![
(Type::Int, Type::Int),
(
Type::List(Box::new(Type::Int)),
Type::List(Box::new(Type::Int)),
),
])
.vectorizes_over_list(true)
.required("bits", SyntaxShape::Int, "number of bits to shift right")
.switch(

View file

@ -15,7 +15,13 @@ impl Command for BitsXor {
fn signature(&self) -> Signature {
Signature::build("bits xor")
.input_output_types(vec![(Type::Int, Type::Int)])
.input_output_types(vec![
(Type::Int, Type::Int),
(
Type::List(Box::new(Type::Int)),
Type::List(Box::new(Type::Int)),
),
])
.vectorizes_over_list(true)
.required(
"target",

View file

@ -16,7 +16,13 @@ impl Command for BytesLen {
fn signature(&self) -> Signature {
Signature::build("bytes length")
.input_output_types(vec![(Type::Binary, Type::Int)])
.input_output_types(vec![
(Type::Binary, Type::Int),
(
Type::List(Box::new(Type::Binary)),
Type::List(Box::new(Type::Int)),
),
])
.vectorizes_over_list(true)
.rest(
"rest",

View file

@ -30,7 +30,10 @@ impl Command for BytesRemove {
fn signature(&self) -> Signature {
Signature::build("bytes remove")
.input_output_types(vec![(Type::Binary, Type::Binary)])
.input_output_types(vec![
(Type::Binary, Type::Binary),
(Type::Table(vec![]), Type::Table(vec![])),
])
.required("pattern", SyntaxShape::Binary, "the pattern to find")
.rest(
"rest",

View file

@ -30,7 +30,10 @@ impl Command for BytesReplace {
fn signature(&self) -> Signature {
Signature::build("bytes replace")
.input_output_types(vec![(Type::Binary, Type::Binary)])
.input_output_types(vec![
(Type::Binary, Type::Binary),
(Type::Table(vec![]), Type::Table(vec![])),
])
.required("find", SyntaxShape::Binary, "the pattern to find")
.required("replace", SyntaxShape::Binary, "the replacement pattern")
.rest(

View file

@ -13,7 +13,13 @@ impl Command for SubCommand {
fn signature(&self) -> Signature {
Signature::build("math cos")
.switch("degrees", "Use degrees instead of radians", Some('d'))
.input_output_types(vec![(Type::Number, Type::Float)])
.input_output_types(vec![
(Type::Number, Type::Float),
(
Type::List(Box::new(Type::Number)),
Type::List(Box::new(Type::Float)),
),
])
.vectorizes_over_list(true)
.category(Category::Math)
}

View file

@ -13,7 +13,13 @@ impl Command for SubCommand {
fn signature(&self) -> Signature {
Signature::build("math sin")
.switch("degrees", "Use degrees instead of radians", Some('d'))
.input_output_types(vec![(Type::Number, Type::Float)])
.input_output_types(vec![
(Type::Number, Type::Float),
(
Type::List(Box::new(Type::Number)),
Type::List(Box::new(Type::Float)),
),
])
.vectorizes_over_list(true)
.category(Category::Math)
}

View file

@ -13,7 +13,13 @@ impl Command for SubCommand {
fn signature(&self) -> Signature {
Signature::build("math tan")
.switch("degrees", "Use degrees instead of radians", Some('d'))
.input_output_types(vec![(Type::Number, Type::Float)])
.input_output_types(vec![
(Type::Number, Type::Float),
(
Type::List(Box::new(Type::Number)),
Type::List(Box::new(Type::Float)),
),
])
.vectorizes_over_list(true)
.category(Category::Math)
}

View file

@ -21,7 +21,7 @@ fn basic_string_fails() {
"#
);
assert!(actual.err.contains("Input type not supported"));
assert!(actual.err.contains("command doesn't support"));
assert_eq!(actual.out, "");
}

View file

@ -16,7 +16,7 @@ impl Command for Collect {
fn signature(&self) -> Signature {
Signature::build("collect")
.input_output_types(vec![(Type::List(Box::new(Type::Any)), Type::Any)])
.input_output_types(vec![(Type::Any, Type::Any)])
.required(
"closure",
SyntaxShape::Closure(Some(vec![SyntaxShape::Any])),

View file

@ -17,15 +17,21 @@ impl Command for SubCommand {
fn signature(&self) -> Signature {
Signature::build("into decimal")
.input_output_types(vec![
(Type::Int, Type::Number),
(Type::String, Type::Number),
(Type::Bool, Type::Number),
(Type::Table(vec![]), Type::Table(vec![])),
(
Type::List(Box::new(Type::Any)),
Type::List(Box::new(Type::Number)),
),
])
.rest(
"rest",
SyntaxShape::CellPath,
"for a data structure input, convert data at the given cell paths",
)
.allow_variants_without_examples(true)
.category(Category::Conversions)
}

View file

@ -36,10 +36,17 @@ impl Command for SubCommand {
(Type::Bool, Type::Int),
// Unix timestamp in nanoseconds
(Type::Date, Type::Int),
(Type::Duration, Type::Int),
// TODO: Users should do this by dividing a Filesize by a Filesize explicitly
(Type::Filesize, Type::Int),
(Type::Table(vec![]), Type::Table(vec![])),
(
Type::List(Box::new(Type::Any)),
Type::List(Box::new(Type::Int)),
),
])
.vectorizes_over_list(true)
.allow_variants_without_examples(true)
.named("radix", SyntaxShape::Number, "radix of integer", Some('r'))
.switch("little-endian", "use little-endian byte decoding", None)
.rest(

View file

@ -40,6 +40,10 @@ impl Command for SubCommand {
(Type::Bool, Type::String),
(Type::Filesize, Type::String),
(Type::Date, Type::String),
(
Type::List(Box::new(Type::Any)),
Type::List(Box::new(Type::String)),
),
])
.allow_variants_without_examples(true) // https://github.com/nushell/nushell/issues/7032
.rest(

View file

@ -19,7 +19,10 @@ impl Command for LoadEnv {
fn signature(&self) -> nu_protocol::Signature {
Signature::build("load-env")
.input_output_types(vec![(Type::Record(vec![]), Type::Nothing)])
.input_output_types(vec![
(Type::Record(vec![]), Type::Nothing),
(Type::Nothing, Type::Nothing),
])
.allow_variants_without_examples(true)
.optional(
"update",

View file

@ -16,11 +16,15 @@ impl Command for Append {
fn signature(&self) -> nu_protocol::Signature {
Signature::build("append")
.input_output_types(vec![(
Type::List(Box::new(Type::Any)),
Type::List(Box::new(Type::Any)),
)])
.input_output_types(vec![
(
Type::List(Box::new(Type::Any)),
Type::List(Box::new(Type::Any)),
),
(Type::Record(vec![]), Type::Table(vec![])),
])
.required("row", SyntaxShape::Any, "the row, list, or table to append")
.allow_variants_without_examples(true)
.category(Category::Filters)
}

View file

@ -41,6 +41,7 @@ with 'transpose' first."#
Type::List(Box::new(Type::Any)),
),
(Type::Table(vec![]), Type::List(Box::new(Type::Any))),
(Type::Any, Type::Any),
])
.required(
"closure",
@ -48,6 +49,7 @@ with 'transpose' first."#
"the closure to run",
)
.switch("keep-empty", "keep empty result cells", Some('k'))
.allow_variants_without_examples(true)
.category(Category::Filters)
}

View file

@ -29,7 +29,7 @@ impl Command for Find {
Type::List(Box::new(Type::Any)),
Type::List(Box::new(Type::Any)),
),
(Type::String, Type::String),
(Type::String, Type::Any),
(
// For find -p
Type::Table(vec![]),

View file

@ -33,12 +33,14 @@ impl Command for First {
Type::Any,
),
(Type::Binary, Type::Binary),
(Type::Range, Type::Any),
])
.optional(
"rows",
SyntaxShape::Int,
"starting from the front, the number of rows to return",
)
.allow_variants_without_examples(true)
.category(Category::Filters)
}

View file

@ -34,6 +34,7 @@ If multiple cell paths are given, this will produce a list of values."#
Type::Any,
),
(Type::Table(vec![]), Type::Any),
(Type::Record(vec![]), Type::Any),
])
.required(
"cell_path",
@ -51,6 +52,7 @@ If multiple cell paths are given, this will produce a list of values."#
"get path in a case sensitive manner",
Some('s'),
)
.allow_variants_without_examples(true)
.category(Category::Filters)
}

View file

@ -19,6 +19,10 @@ impl Command for Insert {
.input_output_types(vec![
(Type::Record(vec![]), Type::Record(vec![])),
(Type::Table(vec![]), Type::Table(vec![])),
(
Type::List(Box::new(Type::Any)),
Type::List(Box::new(Type::Any)),
),
])
.required(
"field",
@ -30,6 +34,7 @@ impl Command for Insert {
SyntaxShape::Any,
"the new value to give the cell(s)",
)
.allow_variants_without_examples(true)
.category(Category::Filters)
}

View file

@ -21,6 +21,7 @@ impl Command for Select {
.input_output_types(vec![
(Type::Record(vec![]), Type::Record(vec![])),
(Type::Table(vec![]), Type::Table(vec![])),
(Type::List(Box::new(Type::Any)), Type::Any),
])
.switch(
"ignore-errors",
@ -32,6 +33,7 @@ impl Command for Select {
SyntaxShape::CellPath,
"the columns to select from the table",
)
.allow_variants_without_examples(true)
.category(Category::Filters)
}

View file

@ -16,7 +16,13 @@ impl Command for SortBy {
fn signature(&self) -> nu_protocol::Signature {
Signature::build("sort-by")
.input_output_types(vec![(Type::Table(vec![]), Type::Table(vec![]))])
.input_output_types(vec![
(Type::Table(vec![]), Type::Table(vec![])),
(
Type::List(Box::new(Type::Any)),
Type::List(Box::new(Type::Any)),
),
])
.rest("columns", SyntaxShape::Any, "the column(s) to sort by")
.switch("reverse", "Sort in reverse order", Some('r'))
.switch(
@ -29,6 +35,7 @@ impl Command for SortBy {
"Sort alphanumeric string-based columns naturally (1, 9, 10, 99, 100, ...)",
Some('n'),
)
.allow_variants_without_examples(true)
.category(Category::Filters)
}

View file

@ -29,6 +29,7 @@ impl Command for Transpose {
.input_output_types(vec![
(Type::Table(vec![]), Type::Table(vec![])),
(Type::Table(vec![]), Type::Record(vec![])),
(Type::Record(vec![]), Type::Table(vec![])),
])
.switch(
"header-row",
@ -55,6 +56,7 @@ impl Command for Transpose {
"on repetition of record fields due to `header-row`, keep all the values obtained",
Some('a'),
)
.allow_variants_without_examples(true)
.rest(
"rest",
SyntaxShape::String,

View file

@ -17,7 +17,13 @@ impl Command for UniqBy {
fn signature(&self) -> Signature {
Signature::build("uniq-by")
.input_output_types(vec![(Type::Table(vec![]), Type::Table(vec![]))])
.input_output_types(vec![
(Type::Table(vec![]), Type::Table(vec![])),
(
Type::List(Box::new(Type::Any)),
Type::List(Box::new(Type::Any)),
),
])
.rest("columns", SyntaxShape::Any, "the column(s) to filter by")
.switch(
"count",
@ -39,6 +45,7 @@ impl Command for UniqBy {
"Return the input values that occur once only",
Some('u'),
)
.allow_variants_without_examples(true)
.category(Category::Filters)
}

View file

@ -19,6 +19,10 @@ impl Command for Update {
.input_output_types(vec![
(Type::Record(vec![]), Type::Record(vec![])),
(Type::Table(vec![]), Type::Table(vec![])),
(
Type::List(Box::new(Type::Any)),
Type::List(Box::new(Type::Any)),
),
])
.required(
"field",
@ -30,6 +34,7 @@ impl Command for Update {
SyntaxShape::Any,
"the new value to give the cell(s), or a closure to create the value",
)
.allow_variants_without_examples(true)
.category(Category::Filters)
}

View file

@ -19,6 +19,10 @@ impl Command for Upsert {
.input_output_types(vec![
(Type::Record(vec![]), Type::Record(vec![])),
(Type::Table(vec![]), Type::Table(vec![])),
(
Type::List(Box::new(Type::Any)),
Type::List(Box::new(Type::Any)),
),
])
.required(
"field",
@ -30,6 +34,7 @@ impl Command for Upsert {
SyntaxShape::Any,
"the new value to give the cell(s), or a closure to create the value",
)
.allow_variants_without_examples(true)
.category(Category::Filters)
}

View file

@ -32,12 +32,14 @@ not supported."#
Type::List(Box::new(Type::Any)),
),
(Type::Table(vec![]), Type::Table(vec![])),
(Type::Range, Type::Any),
])
.required(
"row_condition",
SyntaxShape::RowCondition,
"Filter condition",
)
.allow_variants_without_examples(true)
.category(Category::Filters)
}

View file

@ -23,8 +23,10 @@ impl Command for Wrap {
.input_output_types(vec![
(Type::List(Box::new(Type::Any)), Type::Table(vec![])),
(Type::Range, Type::Table(vec![])),
(Type::Any, Type::Record(vec![])),
])
.required("name", SyntaxShape::String, "the name of the column")
.allow_variants_without_examples(true)
.category(Category::Filters)
}

View file

@ -12,8 +12,15 @@ impl Command for SubCommand {
fn signature(&self) -> Signature {
Signature::build("math abs")
.input_output_types(vec![(Type::Number, Type::Number)])
.input_output_types(vec![
(Type::Number, Type::Number),
(
Type::List(Box::new(Type::Number)),
Type::List(Box::new(Type::Number)),
),
])
.vectorizes_over_list(true)
.allow_variants_without_examples(true)
.category(Category::Math)
}

View file

@ -14,7 +14,12 @@ impl Command for SubCommand {
fn signature(&self) -> Signature {
Signature::build("math avg")
.input_output_types(vec![(Type::List(Box::new(Type::Number)), Type::Number)])
.input_output_types(vec![
(Type::List(Box::new(Type::Number)), Type::Number),
(Type::List(Box::new(Type::Duration)), Type::Duration),
(Type::List(Box::new(Type::Filesize)), Type::Filesize),
])
.allow_variants_without_examples(true)
.category(Category::Math)
}

View file

@ -12,8 +12,15 @@ impl Command for SubCommand {
fn signature(&self) -> Signature {
Signature::build("math ceil")
.input_output_types(vec![(Type::Number, Type::Int)])
.input_output_types(vec![
(Type::Number, Type::Int),
(
Type::List(Box::new(Type::Number)),
Type::List(Box::new(Type::Number)),
),
])
.vectorizes_over_list(true)
.allow_variants_without_examples(true)
.category(Category::Math)
}

View file

@ -12,8 +12,15 @@ impl Command for SubCommand {
fn signature(&self) -> Signature {
Signature::build("math floor")
.input_output_types(vec![(Type::Number, Type::Int)])
.input_output_types(vec![
(Type::Number, Type::Int),
(
Type::List(Box::new(Type::Number)),
Type::List(Box::new(Type::Number)),
),
])
.vectorizes_over_list(true)
.allow_variants_without_examples(true)
.category(Category::Math)
}

View file

@ -20,7 +20,15 @@ impl Command for SubCommand {
SyntaxShape::Number,
"Base for which the logarithm should be computed",
)
.input_output_types(vec![(Type::Number, Type::Float)])
.input_output_types(vec![
(Type::Number, Type::Float),
(Type::Number, Type::Int),
(
Type::List(Box::new(Type::Number)),
Type::List(Box::new(Type::Number)),
),
])
.allow_variants_without_examples(true)
.vectorizes_over_list(true)
.category(Category::Math)
}

View file

@ -18,6 +18,7 @@ impl Command for SubCommand {
(Type::List(Box::new(Type::Number)), Type::Number),
(Type::Table(vec![]), Type::Record(vec![])),
])
.allow_variants_without_examples(true)
.category(Category::Math)
}

View file

@ -20,6 +20,7 @@ impl Command for SubCommand {
(Type::List(Box::new(Type::Number)), Type::Number),
(Type::Table(vec![]), Type::Record(vec![])),
])
.allow_variants_without_examples(true)
.category(Category::Math)
}

View file

@ -18,6 +18,7 @@ impl Command for SubCommand {
(Type::List(Box::new(Type::Number)), Type::Number),
(Type::Table(vec![]), Type::Record(vec![])),
])
.allow_variants_without_examples(true)
.category(Category::Math)
}

View file

@ -45,6 +45,7 @@ impl Command for SubCommand {
),
(Type::Table(vec![]), Type::Record(vec![])),
])
.allow_variants_without_examples(true)
.category(Category::Math)
}

View file

@ -15,7 +15,13 @@ impl Command for SubCommand {
fn signature(&self) -> Signature {
Signature::build("math round")
.input_output_types(vec![(Type::Number, Type::Number)])
.input_output_types(vec![
(Type::Number, Type::Number),
(
Type::List(Box::new(Type::Number)),
Type::List(Box::new(Type::Number)),
),
])
.vectorizes_over_list(true)
.named(
"precision",

View file

@ -12,8 +12,15 @@ impl Command for SubCommand {
fn signature(&self) -> Signature {
Signature::build("math sqrt")
.input_output_types(vec![(Type::Number, Type::Number)])
.input_output_types(vec![
(Type::Number, Type::Number),
(
Type::List(Box::new(Type::Number)),
Type::List(Box::new(Type::Number)),
),
])
.vectorizes_over_list(true)
.allow_variants_without_examples(true)
.category(Category::Math)
}

View file

@ -14,7 +14,12 @@ impl Command for SubCommand {
fn signature(&self) -> Signature {
Signature::build("math sum")
.input_output_types(vec![(Type::List(Box::new(Type::Number)), Type::Number)])
.input_output_types(vec![
(Type::List(Box::new(Type::Number)), Type::Number),
(Type::Range, Type::Number),
(Type::Table(vec![]), Type::Table(vec![])),
])
.allow_variants_without_examples(true)
.category(Category::Math)
}

View file

@ -17,7 +17,7 @@ impl Command for SubCommand {
fn signature(&self) -> Signature {
Signature::build("url encode")
.input_output_types(vec![(Type::String, Type::String)])
.input_output_types(vec![(Type::String, Type::String), (Type::List(Box::new(Type::String)), Type::List(Box::new(Type::String)))])
.vectorizes_over_list(true)
.switch(
"all",

View file

@ -35,6 +35,7 @@ impl Command for SubCommand {
.input_output_types(vec![
(Type::String, Type::String),
(Type::List(Box::new(Type::String)), Type::String),
(Type::Record(vec![]), Type::String),
(Type::Table(vec![]), Type::List(Box::new(Type::String))),
])
.named(
@ -43,6 +44,7 @@ impl Command for SubCommand {
"For a record or table input, join strings at the given columns",
Some('c'),
)
.allow_variants_without_examples(true)
.rest("append", SyntaxShape::String, "Path to append to the input")
}

View file

@ -15,12 +15,13 @@ impl Command for SubCommand {
fn signature(&self) -> Signature {
Signature::build("ansi strip")
.input_output_types(vec![(Type::String, Type::String)])
.input_output_types(vec![(Type::String, Type::String), (Type::List(Box::new(Type::String)), Type::List(Box::new(Type::String)))])
.rest(
"cell path",
SyntaxShape::CellPath,
"for a data structure input, remove ANSI sequences from strings at the given cell paths",
)
.allow_variants_without_examples(true)
.category(Category::Platform)
}

View file

@ -30,8 +30,12 @@ impl Command for Parse {
SyntaxShape::String,
"the pattern to match. Eg) \"{foo}: {bar}\"",
)
.input_output_types(vec![(Type::String, Type::Table(vec![]))])
.input_output_types(vec![
(Type::String, Type::Table(vec![])),
(Type::List(Box::new(Type::Any)), Type::Table(vec![])),
])
.switch("regex", "use full regex syntax for patterns", Some('r'))
.allow_variants_without_examples(true)
.category(Category::Strings)
}

View file

@ -16,8 +16,12 @@ impl Command for SubCommand {
fn signature(&self) -> Signature {
Signature::build("split row")
.input_output_types(vec![(Type::String, Type::List(Box::new(Type::String)))])
.input_output_types(vec![
(Type::String, Type::List(Box::new(Type::String))),
(Type::List(Box::new(Type::String)), Type::Table(vec![])),
])
.vectorizes_over_list(true)
.allow_variants_without_examples(true)
.required(
"separator",
SyntaxShape::String,

View file

@ -17,7 +17,10 @@ impl Command for SubCommand {
fn signature(&self) -> Signature {
Signature::build("str camel-case")
.input_output_types(vec![(Type::String, Type::String)])
.input_output_types(vec![
(Type::String, Type::String),
(Type::Table(vec![]), Type::Table(vec![])),
])
.vectorizes_over_list(true)
.rest(
"rest",

View file

@ -15,7 +15,10 @@ impl Command for SubCommand {
fn signature(&self) -> Signature {
Signature::build("str capitalize")
.input_output_types(vec![(Type::String, Type::String)])
.input_output_types(vec![
(Type::String, Type::String),
(Type::Table(vec![]), Type::Table(vec![])),
])
.vectorizes_over_list(true)
.rest(
"rest",

View file

@ -15,7 +15,10 @@ impl Command for SubCommand {
fn signature(&self) -> Signature {
Signature::build("str downcase")
.input_output_types(vec![(Type::String, Type::String)])
.input_output_types(vec![
(Type::String, Type::String),
(Type::Table(vec![]), Type::Table(vec![])),
])
.vectorizes_over_list(true)
.rest(
"rest",

View file

@ -17,7 +17,10 @@ impl Command for SubCommand {
fn signature(&self) -> Signature {
Signature::build("str kebab-case")
.input_output_types(vec![(Type::String, Type::String)])
.input_output_types(vec![
(Type::String, Type::String),
(Type::Table(vec![]), Type::Table(vec![])),
])
.vectorizes_over_list(true)
.rest(
"rest",

View file

@ -17,7 +17,10 @@ impl Command for SubCommand {
fn signature(&self) -> Signature {
Signature::build("str pascal-case")
.input_output_types(vec![(Type::String, Type::String)])
.input_output_types(vec![
(Type::String, Type::String),
(Type::Table(vec![]), Type::Table(vec![])),
])
.vectorizes_over_list(true)
.rest(
"rest",

View file

@ -16,7 +16,10 @@ impl Command for SubCommand {
fn signature(&self) -> Signature {
Signature::build("str screaming-snake-case")
.input_output_types(vec![(Type::String, Type::String)])
.input_output_types(vec![
(Type::String, Type::String),
(Type::Table(vec![]), Type::Table(vec![])),
])
.vectorizes_over_list(true)
.rest(
"rest",

View file

@ -16,7 +16,10 @@ impl Command for SubCommand {
fn signature(&self) -> Signature {
Signature::build("str snake-case")
.input_output_types(vec![(Type::String, Type::String)])
.input_output_types(vec![
(Type::String, Type::String),
(Type::Table(vec![]), Type::Table(vec![])),
])
.vectorizes_over_list(true)
.rest(
"rest",

View file

@ -17,7 +17,10 @@ impl Command for SubCommand {
fn signature(&self) -> Signature {
Signature::build("str title-case")
.input_output_types(vec![(Type::String, Type::String)])
.input_output_types(vec![
(Type::String, Type::String),
(Type::Table(vec![]), Type::Table(vec![])),
])
.vectorizes_over_list(true)
.rest(
"rest",

View file

@ -14,8 +14,12 @@ impl Command for SubCommand {
fn signature(&self) -> Signature {
Signature::build("str upcase")
.input_output_types(vec![(Type::String, Type::String)])
.input_output_types(vec![
(Type::String, Type::String),
(Type::Table(vec![]), Type::Table(vec![])),
])
.vectorizes_over_list(true)
.allow_variants_without_examples(true)
.rest(
"rest",
SyntaxShape::CellPath,

View file

@ -32,6 +32,7 @@ impl Command for SubCommand {
.input_output_types(vec![
(Type::String, Type::Bool),
(Type::Table(vec![]), Type::Table(vec![])),
(Type::List(Box::new(Type::String)), Type::List(Box::new(Type::Bool)))
])
.vectorizes_over_list(true)
.required("string", SyntaxShape::String, "the substring to find")

View file

@ -16,12 +16,16 @@ impl Command for StrJoin {
fn signature(&self) -> Signature {
Signature::build("str join")
.input_output_types(vec![(Type::List(Box::new(Type::String)), Type::String)])
.input_output_types(vec![
(Type::List(Box::new(Type::Any)), Type::String),
(Type::String, Type::String),
])
.optional(
"separator",
SyntaxShape::String,
"optional separator to use when creating string",
)
.allow_variants_without_examples(true)
.category(Category::Strings)
}

View file

@ -29,7 +29,7 @@ impl Command for SubCommand {
fn signature(&self) -> Signature {
Signature::build("str length")
.input_output_types(vec![(Type::String, Type::Int)])
.input_output_types(vec![(Type::String, Type::Int), (Type::List(Box::new(Type::String)), Type::List(Box::new(Type::Int)))])
.vectorizes_over_list(true)
.switch(
"grapheme-clusters",

View file

@ -34,7 +34,10 @@ impl Command for SubCommand {
fn signature(&self) -> Signature {
Signature::build("str replace")
.input_output_types(vec![(Type::String, Type::String)])
.input_output_types(vec![
(Type::String, Type::String),
(Type::Table(vec![]), Type::Table(vec![])),
])
.vectorizes_over_list(true)
.required("find", SyntaxShape::String, "the pattern to find")
.required("replace", SyntaxShape::String, "the replacement string")

View file

@ -16,7 +16,13 @@ impl Command for SubCommand {
fn signature(&self) -> Signature {
Signature::build("str reverse")
.input_output_types(vec![(Type::String, Type::String)])
.input_output_types(vec![
(Type::String, Type::String),
(
Type::List(Box::new(Type::String)),
Type::List(Box::new(Type::String)),
),
])
.vectorizes_over_list(true)
.rest(
"rest",

View file

@ -42,8 +42,9 @@ impl Command for SubCommand {
fn signature(&self) -> Signature {
Signature::build("str substring")
.input_output_types(vec![(Type::String, Type::String)])
.input_output_types(vec![(Type::String, Type::String), (Type::Table(vec![]), Type::Table(vec![]))])
.vectorizes_over_list(true)
.allow_variants_without_examples(true)
.switch(
"grapheme-clusters",
"count indexes and split using grapheme clusters (all visible chars have length 1)",

View file

@ -35,8 +35,16 @@ impl Command for SubCommand {
fn signature(&self) -> Signature {
Signature::build("str trim")
.input_output_types(vec![(Type::String, Type::String)])
.input_output_types(vec![
(Type::String, Type::String),
(
Type::List(Box::new(Type::String)),
Type::List(Box::new(Type::String)),
),
(Type::Table(vec![]), Type::Table(vec![])),
])
.vectorizes_over_list(true)
.allow_variants_without_examples(true)
.rest(
"rest",
SyntaxShape::CellPath,

View file

@ -91,5 +91,5 @@ fn nth_missing_first_argument() {
fn fail_on_non_iterator() {
let actual = nu!("1 | drop 50");
assert!(actual.err.contains("only_supports_this_input_type"));
assert!(actual.err.contains("command doesn't support"));
}

View file

@ -78,7 +78,7 @@ fn last_errors_on_negative_index() {
fn fail_on_non_iterator() {
let actual = nu!("1 | last");
assert!(actual.err.contains("only_supports_this_input_type"));
assert!(actual.err.contains("command doesn't support"));
}
#[test]

View file

@ -32,5 +32,5 @@ fn can_round_float_with_negative_precision() {
fn fails_with_wrong_input_type() {
let actual = nu!("\"not_a_number\" | math round");
assert!(actual.err.contains("Input type not supported"))
assert!(actual.err.contains("command doesn't support"))
}

View file

@ -70,7 +70,6 @@ fn print_created_paths() {
pipeline(
r#"
mkdir -v dir_1 dir_2 dir_3
| length
"#
));

View file

@ -83,7 +83,7 @@ fn errors_if_no_columns_present() {
"#
));
assert!(actual.err.contains("only record input data is supported"));
assert!(actual.err.contains("command doesn't support"));
})
}

View file

@ -14,5 +14,5 @@ fn can_get_reverse_first() {
fn fail_on_non_iterator() {
let actual = nu!(cwd: ".", pipeline("1 | reverse"));
assert!(actual.err.contains("only_supports_this_input_type"));
assert!(actual.err.contains("command doesn't support"));
}

View file

@ -19,5 +19,5 @@ fn binary_skip() {
fn fail_on_non_iterator() {
let actual = nu!(cwd: ".", pipeline("1 | skip 2"));
assert!(actual.err.contains("only_supports_this_input_type"));
assert!(actual.err.contains("command doesn't support"));
}

View file

@ -54,5 +54,5 @@ fn condition_is_met() {
fn fail_on_non_iterator() {
let actual = nu!(cwd: ".", pipeline("1 | skip until {|row| $row == 2}"));
assert!(actual.err.contains("only_supports_this_input_type"));
assert!(actual.err.contains("command doesn't support"));
}

View file

@ -54,5 +54,5 @@ fn condition_is_met() {
fn fail_on_non_iterator() {
let actual = nu!(cwd: ".", pipeline("1 | skip while {|row| $row == 2}"));
assert!(actual.err.contains("only_supports_this_input_type"));
assert!(actual.err.contains("command doesn't support"));
}

View file

@ -126,5 +126,5 @@ fn no_column_specified_fails() {
fn fail_on_non_iterator() {
let actual = nu!("1 | sort-by");
assert!(actual.err.contains("only_supports_this_input_type"));
assert!(actual.err.contains("command doesn't support"));
}

View file

@ -51,7 +51,7 @@ fn fails_on_string() {
"#
));
assert!(actual.err.contains("only_supports_this_input_type"));
assert!(actual.err.contains("command doesn't support"));
}
#[test]

View file

@ -55,5 +55,5 @@ fn condition_is_met() {
fn fail_on_non_iterator() {
let actual = nu!(cwd: ".", pipeline("1 | take until {|row| $row == 2}"));
assert!(actual.err.contains("only_supports_this_input_type"));
assert!(actual.err.contains("command doesn't support"));
}

View file

@ -54,5 +54,5 @@ fn condition_is_met() {
fn fail_on_non_iterator() {
let actual = nu!(cwd: ".", pipeline("1 | take while {|row| $row == 2}"));
assert!(actual.err.contains("only_supports_this_input_type"));
assert!(actual.err.contains("command doesn't support"));
}

View file

@ -180,7 +180,7 @@ fn contains_operator() {
fn fail_on_non_iterator() {
let actual = nu!(cwd: ".", pipeline(r#"{"name": "foo", "size": 3} | where name == "foo""#));
assert!(actual.err.contains("only_supports_this_input_type"));
assert!(actual.err.contains("command doesn't support"));
}
// Test that filtering on columns that might be missing/null works

View file

@ -405,5 +405,5 @@ fn string_to_csv_error() {
"#
));
assert!(actual.err.contains("can't convert"))
assert!(actual.err.contains("command doesn't support"))
}

View file

@ -70,7 +70,7 @@ fn table_to_toml_fails() {
"#
));
assert_eq!(actual.out, "true");
assert!(actual.err.contains("command doesn't support"));
}
#[test]
@ -83,7 +83,7 @@ fn string_to_toml_fails() {
"#
));
assert_eq!(actual.out, "true");
assert!(actual.err.contains("command doesn't support"));
}
#[test]

View file

@ -4,7 +4,7 @@ use crate::{
lite_parser::{lite_parse, LiteCommand, LiteElement, LitePipeline},
parse_mut,
parse_patterns::{parse_match_pattern, parse_pattern},
type_check::{math_result_type, type_compatible},
type_check::{self, math_result_type, type_compatible},
Token, TokenContents,
};
@ -5582,6 +5582,8 @@ pub fn parse_block(
block.span = Some(span);
type_check::check_block_input_output(working_set, &block);
block
}

View file

@ -1,5 +1,8 @@
use nu_protocol::{
ast::{Bits, Boolean, Comparison, Expr, Expression, Math, Operator},
ast::{
Bits, Block, Boolean, Comparison, Expr, Expression, Math, Operator, Pipeline,
PipelineElement,
},
engine::StateWorkingSet,
ParseError, Type,
};
@ -24,7 +27,30 @@ pub fn type_compatible(lhs: &Type, rhs: &Type) -> bool {
match (lhs, rhs) {
(Type::List(c), Type::List(d)) => type_compatible(c, d),
(Type::List(c), Type::Table(_)) => matches!(**c, Type::Any),
(Type::ListStream, Type::List(_)) => true,
(Type::List(_), Type::ListStream) => true,
(Type::List(c), Type::Table(table_fields)) => {
if matches!(**c, Type::Any) {
return true;
}
if let Type::Record(fields) = &**c {
is_compatible(fields, table_fields)
} else {
false
}
}
(Type::Table(table_fields), Type::List(c)) => {
if matches!(**c, Type::Any) {
return true;
}
if let Type::Record(fields) = &**c {
is_compatible(table_fields, fields)
} else {
false
}
}
(Type::Number, Type::Int) => true,
(Type::Int, Type::Number) => true,
(Type::Number, Type::Float) => true,
@ -921,3 +947,108 @@ pub fn math_result_type(
}
}
}
pub fn check_pipeline_type(
working_set: &mut StateWorkingSet,
pipeline: &Pipeline,
input_type: Type,
) -> Type {
let mut current_type = input_type;
'elem: for elem in &pipeline.elements {
match elem {
PipelineElement::Expression(
_,
Expression {
expr: Expr::Call(call),
..
},
) => {
let decl = working_set.get_decl(call.decl_id);
if current_type == Type::Any {
let mut new_current_type = None;
for (_, call_output) in decl.signature().input_output_types {
if let Some(inner_current_type) = &new_current_type {
if inner_current_type == &Type::Any {
break;
} else if inner_current_type != &call_output {
// Union unequal types to Any for now
new_current_type = Some(Type::Any)
}
} else {
new_current_type = Some(call_output.clone())
}
}
if let Some(new_current_type) = new_current_type {
current_type = new_current_type
} else {
current_type = Type::Any;
}
continue 'elem;
} else {
for (call_input, call_output) in decl.signature().input_output_types {
if type_compatible(&call_input, &current_type) {
current_type = call_output.clone();
continue 'elem;
}
}
}
if !decl.signature().input_output_types.is_empty() {
working_set.error(ParseError::InputMismatch(current_type, call.head))
}
current_type = Type::Any;
}
PipelineElement::Expression(_, Expression { ty, .. }) => {
current_type = ty.clone();
}
_ => {
current_type = Type::Any;
}
}
}
current_type
}
pub fn check_block_input_output(working_set: &mut StateWorkingSet, block: &Block) {
// let inputs = block.input_types();
for (input_type, output_type) in &block.signature.input_output_types {
let mut current_type = input_type.clone();
let mut current_output_type = Type::Nothing;
for pipeline in &block.pipelines {
current_output_type = check_pipeline_type(working_set, pipeline, current_type);
current_type = Type::Nothing;
}
if !type_compatible(output_type, &current_output_type)
&& output_type != &Type::Any
&& current_output_type != Type::Any
{
working_set.error(ParseError::OutputMismatch(
output_type.clone(),
block
.pipelines
.last()
.expect("internal error: we should have pipelines")
.elements
.last()
.expect("internal error: we should have elements")
.span(),
))
}
}
if block.signature.input_output_types.is_empty() {
let mut current_type = Type::Any;
for pipeline in &block.pipelines {
let _ = check_pipeline_type(working_set, pipeline, current_type);
current_type = Type::Nothing;
}
}
}

View file

@ -1,5 +1,5 @@
use super::{Expr, Expression, Pipeline};
use crate::{ast::PipelineElement, engine::StateWorkingSet, Signature, Span, Type, VarId};
use super::Pipeline;
use crate::{ast::PipelineElement, Signature, Span, Type, VarId};
use serde::{Deserialize, Serialize};
use std::ops::{Index, IndexMut};
@ -66,37 +66,6 @@ impl Block {
}
}
pub fn input_type(&self, working_set: &StateWorkingSet) -> Type {
if let Some(first) = self.pipelines.first() {
if let Some(first) = first.elements.first() {
match first {
PipelineElement::Expression(
_,
Expression {
expr: Expr::Call(call),
..
},
) => {
let decl = working_set.get_decl(call.decl_id);
decl.signature().get_input_type()
}
PipelineElement::Expression(
_,
Expression {
expr: Expr::ExternalCall(..),
..
},
) => Type::Any,
_ => Type::Nothing,
}
} else {
Type::Nothing
}
} else {
Type::Nothing
}
}
pub fn output_type(&self) -> Type {
if let Some(last) = self.pipelines.last() {
if let Some(last) = last.elements.last() {

View file

@ -48,6 +48,14 @@ pub enum ParseError {
#[diagnostic(code(nu::parser::parse_mismatch_with_full_string_msg))]
ExpectedWithStringMsg(String, #[label("expected {0}")] Span),
#[error("Command does not support {0} input.")]
#[diagnostic(code(nu::parser::input_type_mismatch))]
InputMismatch(Type, #[label("command doesn't support {0} input")] Span),
#[error("Command output doesn't match {0}.")]
#[diagnostic(code(nu::parser::output_type_mismatch))]
OutputMismatch(Type, #[label("command doesn't output {0}")] Span),
#[error("Type mismatch during operation.")]
#[diagnostic(code(nu::parser::type_mismatch))]
Mismatch(String, String, #[label("expected {0}, found {1}")] Span), // expected, found, span
@ -526,6 +534,8 @@ impl ParseError {
ParseError::KeywordMissingArgument(_, _, s) => *s,
ParseError::MissingType(s) => *s,
ParseError::TypeMismatch(_, _, s) => *s,
ParseError::InputMismatch(_, s) => *s,
ParseError::OutputMismatch(_, s) => *s,
ParseError::MissingRequiredFlag(_, s) => *s,
ParseError::IncompleteMathExpression(s) => *s,
ParseError::UnknownState(_, s) => *s,

View file

@ -46,7 +46,7 @@ fn bits_xor_negative() -> TestResult {
#[test]
fn bits_xor_list() -> TestResult {
run_test(
"[1 2 3 8 9 10] | bits xor 2 | str join '.'",
"[1 2 3 8 9 10] | bits xor 2 | into string | str join '.'",
"3.0.1.10.11.8",
)
}

View file

@ -55,7 +55,7 @@ fn in_and_if_else() -> TestResult {
#[test]
fn help_works_with_missing_requirements() -> TestResult {
// `each while` is part of the *extra* feature and adds 3 lines
let expected_length = if cfg!(feature = "extra") { "65" } else { "62" };
let expected_length = if cfg!(feature = "extra") { "66" } else { "63" };
run_test(r#"each --help | lines | length"#, expected_length)
}