diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index eecd7863a..07ab35dc3 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -3361,26 +3361,66 @@ pub fn parse_input_output_types( } pub fn parse_full_signature(working_set: &mut StateWorkingSet, spans: &[Span]) -> Expression { - let arg_signature = working_set.get_span_contents(spans[0]); + match spans.len() { + // This case should never happen. It corresponds to declarations like `def foo {}`, + // which should throw a 'Missing required positional argument.' before getting to this point + 0 => { + working_set.error(ParseError::InternalError( + "failed to catch missing positional arguments".to_string(), + Span::concat(spans), + )); + garbage(working_set, Span::concat(spans)) + } - if arg_signature.ends_with(b":") { - let mut arg_signature = - parse_signature(working_set, Span::new(spans[0].start, spans[0].end - 1)); + // e.g. `[ b"[foo: string]" ]` + 1 => parse_signature(working_set, spans[0]), - let input_output_types = parse_input_output_types(working_set, &spans[1..]); + // This case is needed to distinguish between e.g. + // `[ b"[]", b"{ true }" ]` vs `[ b"[]:", b"int" ]` + 2 if working_set.get_span_contents(spans[1]).starts_with(b"{") => { + parse_signature(working_set, spans[0]) + } - if let Expression { - expr: Expr::Signature(sig), - span: expr_span, - .. - } = &mut arg_signature - { - sig.input_output_types = input_output_types; - expr_span.end = Span::concat(&spans[1..]).end; + // This should handle every other case, e.g. + // `[ b"[]:", b"int" ]` + // `[ b"[]", b":", b"int" ]` + // `[ b"[]", b":", b"int", b"->", b"bool" ]` + _ => { + let (mut arg_signature, input_output_types_pos) = + if working_set.get_span_contents(spans[0]).ends_with(b":") { + ( + parse_signature(working_set, Span::new(spans[0].start, spans[0].end - 1)), + 1, + ) + } else if working_set.get_span_contents(spans[1]) == b":" { + (parse_signature(working_set, spans[0]), 2) + } else { + // This should be an error case, but we call parse_signature anyway + // so it can handle the various possible errors + // e.g. `[ b"[]", b"int" ]` or `[ + working_set.error(ParseError::Expected( + "colon (:) before type signature", + Span::concat(&spans[1..]), + )); + // (garbage(working_set, Span::concat(spans)), 1) + + (parse_signature(working_set, spans[0]), 1) + }; + + let input_output_types = + parse_input_output_types(working_set, &spans[input_output_types_pos..]); + + if let Expression { + expr: Expr::Signature(sig), + span: expr_span, + .. + } = &mut arg_signature + { + sig.input_output_types = input_output_types; + expr_span.end = Span::concat(&spans[input_output_types_pos..]).end; + } + arg_signature } - arg_signature - } else { - parse_signature(working_set, spans[0]) } } diff --git a/crates/nu-parser/tests/test_parser.rs b/crates/nu-parser/tests/test_parser.rs index 11cca19d1..2ceabf8a1 100644 --- a/crates/nu-parser/tests/test_parser.rs +++ b/crates/nu-parser/tests/test_parser.rs @@ -2460,6 +2460,7 @@ mod input_types { #[rstest] #[case::input_output(b"def q []: int -> int {1}", false)] + #[case::input_output(b"def q [x: bool]: int -> int {2}", false)] #[case::input_output(b"def q []: string -> string {'qwe'}", false)] #[case::input_output(b"def q []: nothing -> nothing {null}", false)] #[case::input_output(b"def q []: list -> list {[]}", false)] @@ -2479,6 +2480,42 @@ mod input_types { #[case::input_output(b"def q []: nothing -> record record {{a: 1}}", true)] #[case::input_output(b"def q []: nothing -> record {{a: {a: 1}}}", true)] + #[case::input_output(b"def q []: int []}", true)] + #[case::input_output(b"def q []: bool {[]", true)] + // Type signature variants with whitespace between inputs and `:` + #[case::input_output(b"def q [] : int -> int {1}", false)] + #[case::input_output(b"def q [x: bool] : int -> int {2}", false)] + #[case::input_output(b"def q []\t : string -> string {'qwe'}", false)] + #[case::input_output(b"def q [] \t : nothing -> nothing {null}", false)] + #[case::input_output(b"def q [] \t: list -> list {[]}", false)] + #[case::input_output( + b"def q []\t: record -> record {{c: 1 e: 1}}", + false + )] + #[case::input_output( + b"def q [] : table -> table {[{c: 1 e: 1}]}", + false + )] + #[case::input_output( + b"def q [] : nothing -> record e: int> {{c: {a: 1 b: 2} e: 1}}", + false + )] + #[case::input_output(b"def q [] : nothing -> list record record {{a: 1}}", true)] + #[case::input_output(b"def q [] : nothing -> record {{a: {a: 1}}}", true)] + #[case::input_output(b"def q [] : int []}", true)] + #[case::input_output(b"def q [] : bool {[]", true)] + // No input-output type signature + #[case::input_output(b"def qq [] {[]}", false)] + #[case::input_output(b"def q [] []}", true)] + #[case::input_output(b"def q [] {", true)] + #[case::input_output(b"def q []: []}", true)] + #[case::input_output(b"def q [] int {}", true)] + #[case::input_output(b"def q [x: string, y: int] {{c: 1 e: 1}}", false)] + #[case::input_output(b"def q [x: string, y: int]: {}", true)] + #[case::input_output(b"def q [x: string, y: int] {a: {a: 1}}", true)] + #[case::input_output(b"def foo {3}", true)] #[case::vardecl(b"let a: int = 1", false)] #[case::vardecl(b"let a: string = 'qwe'", false)] #[case::vardecl(b"let a: nothing = null", false)] diff --git a/crates/nu-std/testing.nu b/crates/nu-std/testing.nu index 7052983a2..e40992056 100644 --- a/crates/nu-std/testing.nu +++ b/crates/nu-std/testing.nu @@ -28,7 +28,7 @@ def valid-annotations [] { # Returns a table containing the list of function names together with their annotations (comments above the declaration) def get-annotated [ file: path -] path -> table { +]: path -> table { let raw_file = ( open $file | lines @@ -59,7 +59,7 @@ def get-annotated [ # Annotations that allow multiple functions are of type list # Other annotations are of type string # Result gets merged with the template record so that the output shape remains consistent regardless of the table content -def create-test-record [] nothing -> record, test-skip: list> { +def create-test-record []: nothing -> record, test-skip: list> { let input = $in let template_record = { @@ -187,7 +187,7 @@ export def ($test_function_name) [] { def run-tests-for-module [ module: record threads: int -] -> table { +]: nothing -> table { let global_context = if not ($module.before-all|is-empty) { log info $"Running before-all for module ($module.name)" run-test {