mirror of
https://github.com/nushell/nushell
synced 2024-12-28 14:03:09 +00:00
ef05d1419d
Manual checks are added to `parse_let`, `parse_mut`, and `parse_const`. `parse_var_with_opt_type` is also fixed to update `spans_idx` correctly. Fixes #12125. It's technically a fix, but I'd rather not merge this directly. I'm making this PR to bring into attention the code quality of the parser code. For example: * Inconsistent usage of `spans_idx`. What is its purpose, and which parsing functions need it? I suspect it's possible to remove the usage of `spans_idx` entirely. * Lacking documentation for top-level functions. What does `mutable` mean for `parse_var_with_opt_type()`? * Inconsistent error reporting. Usage of both `working_set.error()` and `working_set.parse_errors.push()`. Using `ParseError::Expected` for an invalid variable name when there's `ParseError::VariableNotValid` (from `parser.rs:5237`). Checking variable names manually when there's `is_variable()` (from `parser.rs:2905`). * `span()` is a terrible name for a function that flattens a bunch of spans into one (from `nu-protocal/src/span.rs:92`). The top-level comment (`Used when you have a slice of spans of at least size 1`) doesn't help either. I've only looked at a small portion of the parser code; I expect there are a lot more. These issues made it much harder to fix a simple bug like #12125. I believe we should invest some effort to cleanup the parser code, which will ease maintainance in the future. I'll willing to help if there is interest.
351 lines
9.1 KiB
Rust
351 lines
9.1 KiB
Rust
use nu_test_support::fs::Stub::FileWithContentToBeTrimmed;
|
|
use nu_test_support::playground::Playground;
|
|
use nu_test_support::{nu, pipeline};
|
|
use pretty_assertions::assert_eq;
|
|
use rstest::rstest;
|
|
|
|
#[test]
|
|
fn source_file_relative_to_file() {
|
|
let actual = nu!(cwd: "tests/parsing/samples", "
|
|
nu source_file_relative.nu
|
|
");
|
|
|
|
assert_eq!(actual.out, "5");
|
|
}
|
|
|
|
#[test]
|
|
fn source_const_file() {
|
|
let actual = nu!(cwd: "tests/parsing/samples",
|
|
"
|
|
const file = 'single_line.nu'
|
|
source $file
|
|
");
|
|
|
|
assert_eq!(actual.out, "5");
|
|
}
|
|
|
|
#[test]
|
|
fn run_nu_script_single_line() {
|
|
let actual = nu!(cwd: "tests/parsing/samples", "
|
|
nu single_line.nu
|
|
");
|
|
|
|
assert_eq!(actual.out, "5");
|
|
}
|
|
|
|
#[test]
|
|
fn run_nu_script_multiline_start_pipe() {
|
|
let actual = nu!(cwd: "tests/parsing/samples", "
|
|
nu multiline_start_pipe.nu
|
|
");
|
|
|
|
assert_eq!(actual.out, "4");
|
|
}
|
|
|
|
#[test]
|
|
fn run_nu_script_multiline_start_pipe_win() {
|
|
let actual = nu!(cwd: "tests/parsing/samples", "
|
|
nu multiline_start_pipe_win.nu
|
|
");
|
|
|
|
assert_eq!(actual.out, "3");
|
|
}
|
|
|
|
#[test]
|
|
fn run_nu_script_multiline_end_pipe() {
|
|
let actual = nu!(cwd: "tests/parsing/samples", "
|
|
nu multiline_end_pipe.nu
|
|
");
|
|
|
|
assert_eq!(actual.out, "2");
|
|
}
|
|
|
|
#[test]
|
|
fn run_nu_script_multiline_end_pipe_win() {
|
|
let actual = nu!(cwd: "tests/parsing/samples", "
|
|
nu multiline_end_pipe_win.nu
|
|
");
|
|
|
|
assert_eq!(actual.out, "3");
|
|
}
|
|
|
|
#[test]
|
|
fn parse_file_relative_to_parsed_file_simple() {
|
|
Playground::setup("relative_files_simple", |dirs, sandbox| {
|
|
sandbox
|
|
.mkdir("lol")
|
|
.mkdir("lol/lol")
|
|
.with_files(vec![FileWithContentToBeTrimmed(
|
|
"lol/lol/lol.nu",
|
|
"
|
|
use ../lol_shell.nu
|
|
|
|
$env.LOL = (lol_shell ls)
|
|
",
|
|
)])
|
|
.with_files(vec![FileWithContentToBeTrimmed(
|
|
"lol/lol_shell.nu",
|
|
r#"
|
|
export def ls [] { "lol" }
|
|
"#,
|
|
)]);
|
|
|
|
let actual = nu!(
|
|
cwd: dirs.test(), pipeline(
|
|
"
|
|
source-env lol/lol/lol.nu;
|
|
$env.LOL
|
|
"
|
|
));
|
|
|
|
assert_eq!(actual.out, "lol");
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn predecl_signature_single_inp_out_type() {
|
|
Playground::setup("predecl_signature_single_inp_out_type", |dirs, sandbox| {
|
|
sandbox.with_files(vec![FileWithContentToBeTrimmed(
|
|
"spam1.nu",
|
|
"
|
|
def main [] { foo }
|
|
|
|
def foo []: nothing -> nothing { print 'foo' }
|
|
",
|
|
)]);
|
|
|
|
let actual = nu!(cwd: dirs.test(), pipeline("nu spam1.nu"));
|
|
|
|
assert_eq!(actual.out, "foo");
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn predecl_signature_multiple_inp_out_types() {
|
|
Playground::setup(
|
|
"predecl_signature_multiple_inp_out_types",
|
|
|dirs, sandbox| {
|
|
sandbox.with_files(vec![FileWithContentToBeTrimmed(
|
|
"spam2.nu",
|
|
"
|
|
def main [] { foo }
|
|
|
|
def foo []: [nothing -> string, string -> string] { 'foo' }
|
|
",
|
|
)]);
|
|
|
|
let actual = nu!(cwd: dirs.test(), pipeline("nu spam2.nu"));
|
|
|
|
assert_eq!(actual.out, "foo");
|
|
},
|
|
)
|
|
}
|
|
|
|
#[ignore]
|
|
#[test]
|
|
fn parse_file_relative_to_parsed_file() {
|
|
Playground::setup("relative_files", |dirs, sandbox| {
|
|
sandbox
|
|
.mkdir("lol")
|
|
.mkdir("lol/lol")
|
|
.with_files(vec![FileWithContentToBeTrimmed(
|
|
"lol/lol/lol.nu",
|
|
"
|
|
source-env ../../foo.nu
|
|
use ../lol_shell.nu
|
|
overlay use ../../lol/lol_shell.nu
|
|
|
|
$env.LOL = $'($env.FOO) (lol_shell ls) (ls)'
|
|
",
|
|
)])
|
|
.with_files(vec![FileWithContentToBeTrimmed(
|
|
"lol/lol_shell.nu",
|
|
r#"
|
|
export def ls [] { "lol" }
|
|
"#,
|
|
)])
|
|
.with_files(vec![FileWithContentToBeTrimmed(
|
|
"foo.nu",
|
|
"
|
|
$env.FOO = 'foo'
|
|
",
|
|
)]);
|
|
|
|
let actual = nu!(
|
|
cwd: dirs.test(), pipeline(
|
|
"
|
|
source-env lol/lol/lol.nu;
|
|
$env.LOL
|
|
"
|
|
));
|
|
|
|
assert_eq!(actual.out, "foo lol lol");
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn parse_file_relative_to_parsed_file_dont_use_cwd_1() {
|
|
Playground::setup("relative_files", |dirs, sandbox| {
|
|
sandbox
|
|
.mkdir("lol")
|
|
.with_files(vec![FileWithContentToBeTrimmed(
|
|
"lol/lol.nu",
|
|
"
|
|
source-env foo.nu
|
|
",
|
|
)])
|
|
.with_files(vec![FileWithContentToBeTrimmed(
|
|
"lol/foo.nu",
|
|
"
|
|
$env.FOO = 'good'
|
|
",
|
|
)])
|
|
.with_files(vec![FileWithContentToBeTrimmed(
|
|
"foo.nu",
|
|
"
|
|
$env.FOO = 'bad'
|
|
",
|
|
)]);
|
|
|
|
let actual = nu!(
|
|
cwd: dirs.test(), pipeline(
|
|
"
|
|
source-env lol/lol.nu;
|
|
$env.FOO
|
|
"
|
|
));
|
|
|
|
assert_eq!(actual.out, "good");
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn parse_file_relative_to_parsed_file_dont_use_cwd_2() {
|
|
Playground::setup("relative_files", |dirs, sandbox| {
|
|
sandbox
|
|
.mkdir("lol")
|
|
.with_files(vec![FileWithContentToBeTrimmed(
|
|
"lol/lol.nu",
|
|
"
|
|
source-env foo.nu
|
|
",
|
|
)])
|
|
.with_files(vec![FileWithContentToBeTrimmed(
|
|
"foo.nu",
|
|
"
|
|
$env.FOO = 'bad'
|
|
",
|
|
)]);
|
|
|
|
let actual = nu!(
|
|
cwd: dirs.test(), pipeline(
|
|
"
|
|
source-env lol/lol.nu
|
|
"
|
|
));
|
|
|
|
assert!(actual.err.contains("File not found"));
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn parse_export_env_in_module() {
|
|
let actual = nu!("
|
|
module spam { export-env { } }
|
|
");
|
|
|
|
assert!(actual.err.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_export_env_missing_block() {
|
|
let actual = nu!("
|
|
module spam { export-env }
|
|
");
|
|
|
|
assert!(actual.err.contains("missing block"));
|
|
}
|
|
|
|
#[test]
|
|
fn call_command_with_non_ascii_argument() {
|
|
let actual = nu!("
|
|
def nu-arg [--umlaut(-ö): int] {}
|
|
nu-arg -ö 42
|
|
");
|
|
|
|
assert_eq!(actual.err.len(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_long_duration() {
|
|
let actual = nu!(r#"
|
|
"78.797877879789789sec" | into duration
|
|
"#);
|
|
|
|
assert_eq!(actual.out, "1min 18sec 797ms");
|
|
}
|
|
|
|
#[rstest]
|
|
#[case("def test [ --a: any = 32 ] {}")]
|
|
#[case("def test [ --a: number = 32 ] {}")]
|
|
#[case("def test [ --a: number = 32.0 ] {}")]
|
|
#[case("def test [ --a: list<any> = [ 1 2 3 ] ] {}")]
|
|
#[case("def test [ --a: record<a: int b: string> = { a: 32 b: 'qwe' c: 'wqe' } ] {}")]
|
|
#[case("def test [ --a: record<a: any b: any> = { a: 32 b: 'qwe'} ] {}")]
|
|
#[case("def test []: int -> int { 1 }")]
|
|
#[case("def test []: string -> string { 'qwe' }")]
|
|
#[case("def test []: nothing -> nothing { null }")]
|
|
#[case("def test []: list<string> -> list<string> { [] }")]
|
|
#[case("def test []: record<a: int b: int> -> record<c: int e: int> { {c: 1 e: 1} }")]
|
|
#[case("def test []: table<a: int b: int> -> table<c: int e: int> { [ {c: 1 e: 1} ] }")]
|
|
#[case("def test []: nothing -> record<c: int e: int> { {c: 1 e: 1} }")]
|
|
fn parse_function_signature(#[case] phrase: &str) {
|
|
let actual = nu!(phrase);
|
|
assert!(actual.err.is_empty());
|
|
}
|
|
|
|
#[rstest]
|
|
#[case("def test [ in ] {}")]
|
|
#[case("def test [ in: string ] {}")]
|
|
#[case("def test [ nu: int ] {}")]
|
|
#[case("def test [ env: record<> ] {}")]
|
|
#[case("def test [ --env ] {}")]
|
|
#[case("def test [ --nu: int ] {}")]
|
|
#[case("def test [ --in (-i): list<any> ] {}")]
|
|
#[case("def test [ a: string, b: int, in: table<a: int b: int> ] {}")]
|
|
#[case("def test [ env, in, nu ] {}")]
|
|
fn parse_function_signature_name_is_builtin_var(#[case] phrase: &str) {
|
|
let actual = nu!(phrase);
|
|
assert!(actual.err.contains("nu::parser::name_is_builtin_var"))
|
|
}
|
|
|
|
#[rstest]
|
|
#[case("let a: int = 1")]
|
|
#[case("let a: string = 'qwe'")]
|
|
#[case("let a: nothing = null")]
|
|
#[case("let a: list<string> = []")]
|
|
#[case("let a: record<a: int b: int> = {a: 1 b: 1}")]
|
|
#[case("let a: table<a: int b: int> = [[a b]; [1 2] [3 4]]")]
|
|
#[case("let a: record<a: record<name: string> b: int> = {a: {name: bob} b: 1}")]
|
|
fn parse_let_signature(#[case] phrase: &str) {
|
|
let actual = nu!(phrase);
|
|
assert!(actual.err.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_let_signature_missing_colon() {
|
|
let actual = nu!("let a int = 1");
|
|
assert!(actual.err.contains("nu::parser::extra_tokens"));
|
|
}
|
|
|
|
#[test]
|
|
fn parse_mut_signature_missing_colon() {
|
|
let actual = nu!("mut a record<a: int b: int> = {a: 1 b: 1}");
|
|
assert!(actual.err.contains("nu::parser::extra_tokens"));
|
|
}
|
|
|
|
#[test]
|
|
fn parse_const_signature_missing_colon() {
|
|
let actual = nu!("const a string = 'Hello World\n'");
|
|
assert!(actual.err.contains("nu::parser::extra_tokens"));
|
|
}
|