allow records to have type annotations (#8914)

# Description
follow up to #8529
cleaned up version of #8892 

- the original syntax is okay
```nu
def okay [rec: record] {}
```
- you can now add type annotations for fields if you know
  them before hand
```nu
def okay [rec: record<name: string>] {}
```

- you can specify multiple fields
```nu
def okay [person: record<name: string age: int>] {}

# an optional comma is allowed
def okay [person: record<name: string, age: int>] {}
```

- if annotations are specified, any use of the command will be type
  checked against the specified type
```nu
def unwrap [result: record<ok: bool, value: any>] {}

unwrap {ok: 2, value: "value"}

# errors with

Error: nu::parser::type_mismatch

  × Type mismatch.
   ╭─[entry #4:1:1]
 1 │ unwrap {ok: 2, value: "value"}
   ·         ───────┬─────
   ·                    ╰── expected record<ok: bool, value: any>, found record<ok: int, value: string>
   ╰────
```
> here the error is in the `ok` field, since `any` is coerced into any
type
> as a result `unwrap {ok: true, value: "value"}` is okay

- the key must be a string, either quoted or unquoted
```nu
def err [rec: record<{}: list>] {}

# errors with
Error:
  × `record` type annotations key not string
   ╭─[entry #7:1:1]
 1 │ def unwrap [result: record<{}: bool, value: any>] {}
   ·                            ─┬
   ·                             ╰── must be a string
   ╰────
```

- a key doesn't have to have a type in which case it is assumed to be
`any`
```nu
def okay [person: record<name age>] {}

def okay [person: record<name: string age>] {}
```

- however, if you put a colon, you have to specify a type
```nu
def err [person: record<name: >] {}

# errors with
Error: nu::parser::parse_mismatch

  × Parse mismatch during operation.
   ╭─[entry #12:1:1]
 1 │ def unwrap [res: record<name: >] { $res }
   ·                             ┬
   ·                             ╰── expected type after colon
   ╰────
```

# User-Facing Changes
**[BREAKING CHANGES]**
- this change adds a field to `SyntaxShape::Record` so any plugins that
used it will have to update and include the field. though if you are
unsure of the type the record expects, `SyntaxShape::Record(vec![])`
will suffice
This commit is contained in:
mike
2023-04-26 16:16:55 +03:00
committed by GitHub
parent 48c75831fc
commit 77ca73f414
8 changed files with 347 additions and 93 deletions

View File

@ -318,7 +318,7 @@ fn default_value11() -> TestResult {
fn default_value12() -> TestResult {
fail_test(
r#"def foo [--x:int = "a"] { $x }"#,
"default value should be int",
"expected default value to be `int`",
)
}

View File

@ -132,3 +132,133 @@ fn list_annotations_with_extra_characters() -> TestResult {
let expected = "Extra characters in the parameter name";
fail_test(input, expected)
}
#[test]
fn record_annotations_none() -> TestResult {
let input = "def run [rec: record] { $rec }; run {} | describe";
let expected = "record";
run_test(input, expected)
}
#[test]
fn record_annotations() -> TestResult {
let input = "def run [rec: record<age: int>] { $rec }; run {age: 3} | describe";
let expected = "record<age: int>";
run_test(input, expected)
}
#[test]
fn record_annotations_two_types() -> TestResult {
let input = "def run [rec: record<name: string age: int>] { $rec }; run {name: nushell age: 3} | describe";
let expected = "record<name: string, age: int>";
run_test(input, expected)
}
#[test]
fn record_annotations_two_types_comma_sep() -> TestResult {
let input = "def run [rec: record<name: string, age: int>] { $rec }; run {name: nushell age: 3} | describe";
let expected = "record<name: string, age: int>";
run_test(input, expected)
}
#[test]
fn record_annotations_key_with_no_type() -> TestResult {
let input = "def run [rec: record<name>] { $rec }; run {name: nushell} | describe";
let expected = "record<name: string>";
run_test(input, expected)
}
#[test]
fn record_annotations_two_types_one_with_no_type() -> TestResult {
let input =
"def run [rec: record<name: string, age>] { $rec }; run {name: nushell age: 3} | describe";
let expected = "record<name: string, age: int>";
run_test(input, expected)
}
#[test]
fn record_annotations_two_types_both_with_no_types() -> TestResult {
let input = "def run [rec: record<name age>] { $rec }; run {name: nushell age: 3} | describe";
let expected = "record<name: string, age: int>";
run_test(input, expected)
}
#[test]
fn record_annotations_nested() -> TestResult {
let input = "def run [
err: record<
msg: string,
label: record<
text: string
start: int,
end: int,
>>
] {
$err
}; run {
msg: 'error message'
label: {
text: 'here is the error'
start: 0
end: 69
}
} | describe";
let expected = "record<msg: string, label: record<text: string, start: int, end: int>>";
run_test(input, expected)
}
#[test]
fn record_annotations_type_inference_1() -> TestResult {
let input = "def run [rec: record<age: any>] { $rec }; run {age: 2wk} | describe";
let expected = "record<age: duration>";
run_test(input, expected)
}
#[test]
fn record_annotations_type_inference_2() -> TestResult {
let input = "def run [rec: record<size>] { $rec }; run {size: 2mb} | describe";
let expected = "record<size: filesize>";
run_test(input, expected)
}
#[test]
fn record_annotations_not_terminated() -> TestResult {
let input = "def run [rec: record<age: int] { $rec }";
let expected = "expected closing >";
fail_test(input, expected)
}
#[test]
fn record_annotations_not_terminated_inner() -> TestResult {
let input = "def run [rec: record<name: string, repos: list<string>] { $rec }";
let expected = "expected closing >";
fail_test(input, expected)
}
#[test]
fn record_annotations_no_type_after_colon() -> TestResult {
let input = "def run [rec: record<name: >] { $rec }";
let expected = "type after colon";
fail_test(input, expected)
}
#[test]
fn record_annotations_type_mismatch_key() -> TestResult {
let input = "def run [rec: record<name: string>] { $rec }; run {nme: nushell}";
let expected = "expected record<name: string>, found record<nme: string>";
fail_test(input, expected)
}
#[test]
fn record_annotations_type_mismatch_shape() -> TestResult {
let input = "def run [rec: record<age: int>] { $rec }; run {age: 2wk}";
let expected = "expected record<age: int>, found record<age: duration>";
fail_test(input, expected)
}
#[test]
fn record_annotations_with_extra_characters() -> TestResult {
let input = "def run [list: record<int>extra] {$list | length}; run [1 2 3]";
let expected = "Extra characters in the parameter name";
fail_test(input, expected)
}