From 2cffff0c1bd8e155d1718702f25e5f99c2f4ff74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BD=C3=A1dn=C3=ADk?= Date: Fri, 29 Jul 2022 11:57:10 +0300 Subject: [PATCH] Allow modules to `use` other modules (#6162) * Allow private imports inside modules Can call `use ...` inside modules now. * Add more tests * Add a leak test * Refactor exportables; Prepare for 'export use' * Fix description * Implement 'export use' command This allows re-exporting module's commands and aliases from another module. * Add more tests; Fix import pattern list strings The import pattern strings didn't trim the surrounding quotes. * Add ignored test --- crates/nu-command/src/core_commands/export.rs | 2 +- .../src/core_commands/export_use.rs | 56 +++ crates/nu-command/src/core_commands/mod.rs | 2 + crates/nu-command/src/default_context.rs | 1 + crates/nu-parser/src/parse_keywords.rs | 298 +++++++++------ crates/nu-parser/src/parser.rs | 12 +- crates/nu-protocol/src/exportable.rs | 6 +- crates/nu-protocol/src/module.rs | 12 +- tests/main.rs | 1 + tests/modules/mod.rs | 347 ++++++++++++++++++ 10 files changed, 620 insertions(+), 117 deletions(-) create mode 100644 crates/nu-command/src/core_commands/export_use.rs create mode 100644 tests/modules/mod.rs diff --git a/crates/nu-command/src/core_commands/export.rs b/crates/nu-command/src/core_commands/export.rs index 7867b20a47..589170cd46 100644 --- a/crates/nu-command/src/core_commands/export.rs +++ b/crates/nu-command/src/core_commands/export.rs @@ -18,7 +18,7 @@ impl Command for ExportCommand { } fn usage(&self) -> &str { - "Export custom commands or environment variables from a module." + "Export definitions or environment variables from a module." } fn extra_usage(&self) -> &str { diff --git a/crates/nu-command/src/core_commands/export_use.rs b/crates/nu-command/src/core_commands/export_use.rs new file mode 100644 index 0000000000..20bf53a50d --- /dev/null +++ b/crates/nu-command/src/core_commands/export_use.rs @@ -0,0 +1,56 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, Example, PipelineData, Signature, Span, SyntaxShape, Value}; + +#[derive(Clone)] +pub struct ExportUse; + +impl Command for ExportUse { + fn name(&self) -> &str { + "export use" + } + + fn usage(&self) -> &str { + "Use definitions from a module and export them from this module" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("export use") + .required("pattern", SyntaxShape::ImportPattern, "import pattern") + .category(Category::Core) + } + + fn extra_usage(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nushell.html"# + } + + fn is_parser_keyword(&self) -> bool { + true + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + Ok(PipelineData::new(call.head)) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Re-export a command from another module", + example: r#"module spam { export def foo [] { "foo" } } + module eggs { export use spam foo } + use eggs foo + foo + "#, + result: Some(Value::String { + val: "foo".to_string(), + span: Span::test_data(), + }), + }] + } +} diff --git a/crates/nu-command/src/core_commands/mod.rs b/crates/nu-command/src/core_commands/mod.rs index f099158b88..6a8bbf476c 100644 --- a/crates/nu-command/src/core_commands/mod.rs +++ b/crates/nu-command/src/core_commands/mod.rs @@ -12,6 +12,7 @@ mod export_def; mod export_def_env; mod export_env; mod export_extern; +mod export_use; mod extern_; mod for_; pub mod help; @@ -40,6 +41,7 @@ pub use export_def::ExportDef; pub use export_def_env::ExportDefEnv; pub use export_env::ExportEnv; pub use export_extern::ExportExtern; +pub use export_use::ExportUse; pub use extern_::Extern; pub use for_::For; pub use help::Help; diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index c113df36d7..0028b4a93e 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -42,6 +42,7 @@ pub fn create_default_context() -> EngineState { ExportDefEnv, ExportEnv, ExportExtern, + ExportUse, Extern, For, Help, diff --git a/crates/nu-parser/src/parse_keywords.rs b/crates/nu-parser/src/parse_keywords.rs index 9dc548b79f..8fd3cf16ff 100644 --- a/crates/nu-parser/src/parse_keywords.rs +++ b/crates/nu-parser/src/parse_keywords.rs @@ -606,7 +606,7 @@ pub fn parse_export( working_set: &mut StateWorkingSet, lite_command: &LiteCommand, expand_aliases_denylist: &[usize], -) -> (Pipeline, Option, Option) { +) -> (Pipeline, Vec, Option) { let spans = &lite_command.parts[..]; let mut error = None; @@ -614,7 +614,7 @@ pub fn parse_export( if working_set.get_span_contents(*sp) != b"export" { return ( garbage_pipeline(spans), - None, + vec![], Some(ParseError::UnknownState( "expected export statement".into(), span(spans), @@ -626,7 +626,7 @@ pub fn parse_export( } else { return ( garbage_pipeline(spans), - None, + vec![], Some(ParseError::UnknownState( "got empty input for parsing export statement".into(), span(spans), @@ -639,7 +639,7 @@ pub fn parse_export( } else { return ( garbage_pipeline(spans), - None, + vec![], Some(ParseError::InternalError( "missing export command".into(), export_span, @@ -655,7 +655,7 @@ pub fn parse_export( redirect_stderr: false, }); - let exportable = if let Some(kw_span) = spans.get(1) { + let exportables = if let Some(kw_span) = spans.get(1) { let kw_name = working_set.get_span_contents(*kw_span); match kw_name { b"def" => { @@ -673,7 +673,7 @@ pub fn parse_export( } else { return ( garbage_pipeline(spans), - None, + vec![], Some(ParseError::InternalError( "missing 'export def' command".into(), export_span, @@ -700,23 +700,26 @@ pub fn parse_export( }); }; - if error.is_none() { - let decl_name = working_set.get_span_contents(spans[2]); - let decl_name = trim_quotes(decl_name); - if let Some(decl_id) = working_set.find_decl(decl_name, &Type::Any) { - Some(Exportable::Decl(decl_id)) - } else { - error = error.or_else(|| { - Some(ParseError::InternalError( - "failed to find added declaration".into(), - span(&spans[1..]), - )) - }); - None - } + let mut result = vec![]; + + let decl_name = working_set.get_span_contents(spans[2]); + let decl_name = trim_quotes(decl_name); + + if let Some(decl_id) = working_set.find_decl(decl_name, &Type::Any) { + result.push(Exportable::Decl { + name: decl_name.to_vec(), + id: decl_id, + }); } else { - None + error = error.or_else(|| { + Some(ParseError::InternalError( + "failed to find added declaration".into(), + span(&spans[1..]), + )) + }); } + + result } b"def-env" => { let lite_command = LiteCommand { @@ -733,7 +736,7 @@ pub fn parse_export( } else { return ( garbage_pipeline(spans), - None, + vec![], Some(ParseError::InternalError( "missing 'export def-env' command".into(), export_span, @@ -760,23 +763,26 @@ pub fn parse_export( }); }; - if error.is_none() { - let decl_name = working_set.get_span_contents(spans[2]); - let decl_name = trim_quotes(decl_name); - if let Some(decl_id) = working_set.find_decl(decl_name, &Type::Any) { - Some(Exportable::Decl(decl_id)) - } else { - error = error.or_else(|| { - Some(ParseError::InternalError( - "failed to find added declaration".into(), - span(&spans[1..]), - )) - }); - None - } + let mut result = vec![]; + + let decl_name = working_set.get_span_contents(spans[2]); + let decl_name = trim_quotes(decl_name); + + if let Some(decl_id) = working_set.find_decl(decl_name, &Type::Any) { + result.push(Exportable::Decl { + name: decl_name.to_vec(), + id: decl_id, + }); } else { - None + error = error.or_else(|| { + Some(ParseError::InternalError( + "failed to find added declaration".into(), + span(&spans[1..]), + )) + }); } + + result } b"extern" => { let lite_command = LiteCommand { @@ -793,7 +799,7 @@ pub fn parse_export( } else { return ( garbage_pipeline(spans), - None, + vec![], Some(ParseError::InternalError( "missing 'export extern' command".into(), export_span, @@ -820,23 +826,26 @@ pub fn parse_export( }); }; - if error.is_none() { - let decl_name = working_set.get_span_contents(spans[2]); - let decl_name = trim_quotes(decl_name); - if let Some(decl_id) = working_set.find_decl(decl_name, &Type::Any) { - Some(Exportable::Decl(decl_id)) - } else { - error = error.or_else(|| { - Some(ParseError::InternalError( - "failed to find added declaration".into(), - span(&spans[1..]), - )) - }); - None - } + let mut result = vec![]; + + let decl_name = working_set.get_span_contents(spans[2]); + let decl_name = trim_quotes(decl_name); + + if let Some(decl_id) = working_set.find_decl(decl_name, &Type::Any) { + result.push(Exportable::Decl { + name: decl_name.to_vec(), + id: decl_id, + }); } else { - None + error = error.or_else(|| { + Some(ParseError::InternalError( + "failed to find added declaration".into(), + span(&spans[1..]), + )) + }); } + + result } b"alias" => { let lite_command = LiteCommand { @@ -853,7 +862,7 @@ pub fn parse_export( } else { return ( garbage_pipeline(spans), - None, + vec![], Some(ParseError::InternalError( "missing 'export alias' command".into(), export_span, @@ -880,23 +889,70 @@ pub fn parse_export( }); }; - if error.is_none() { - let alias_name = working_set.get_span_contents(spans[2]); - let alias_name = trim_quotes(alias_name); - if let Some(alias_id) = working_set.find_alias(alias_name) { - Some(Exportable::Alias(alias_id)) - } else { - error = error.or_else(|| { - Some(ParseError::InternalError( - "failed to find added alias".into(), - span(&spans[1..]), - )) - }); - None - } + let mut result = vec![]; + + let alias_name = working_set.get_span_contents(spans[2]); + let alias_name = trim_quotes(alias_name); + + if let Some(alias_id) = working_set.find_alias(alias_name) { + result.push(Exportable::Alias { + name: alias_name.to_vec(), + id: alias_id, + }); } else { - None + error = error.or_else(|| { + Some(ParseError::InternalError( + "failed to find added alias".into(), + span(&spans[1..]), + )) + }); } + + result + } + b"use" => { + let lite_command = LiteCommand { + comments: lite_command.comments.clone(), + parts: spans[1..].to_vec(), + }; + let (pipeline, exportables, err) = + parse_use(working_set, &lite_command.parts, expand_aliases_denylist); + error = error.or(err); + + let export_use_decl_id = + if let Some(id) = working_set.find_decl(b"export use", &Type::Any) { + id + } else { + return ( + garbage_pipeline(spans), + vec![], + Some(ParseError::InternalError( + "missing 'export use' command".into(), + export_span, + )), + ); + }; + + // Trying to warp the 'use' call into the 'export use' in a very clumsy way + if let Some(Expression { + expr: Expr::Call(ref use_call), + .. + }) = pipeline.expressions.get(0) + { + call = use_call.clone(); + + call.head = span(&spans[0..=1]); + call.decl_id = export_use_decl_id; + } else { + error = error.or_else(|| { + Some(ParseError::InternalError( + "unexpected output from parsing a definition".into(), + span(&spans[1..]), + )) + }); + }; + + exportables } b"env" => { if let Some(id) = working_set.find_decl(b"export env", &Type::Any) { @@ -904,7 +960,7 @@ pub fn parse_export( } else { return ( garbage_pipeline(spans), - None, + vec![], Some(ParseError::InternalError( "missing 'export env' command".into(), export_span, @@ -917,12 +973,16 @@ pub fn parse_export( call.head = span(&spans[0..=1]); + let mut result = vec![]; + if let Some(name_span) = spans.get(2) { let (name_expr, err) = parse_string(working_set, *name_span, expand_aliases_denylist); error = error.or(err); call.add_positional(name_expr); + let env_var_name = working_set.get_span_contents(*name_span).to_vec(); + if let Some(block_span) = spans.get(3) { let (block_expr, err) = parse_block_expression( working_set, @@ -932,12 +992,15 @@ pub fn parse_export( ); error = error.or(err); - let exportable = if let Expression { + if let Expression { expr: Expr::Block(block_id), .. } = block_expr { - Some(Exportable::EnvVar(block_id)) + result.push(Exportable::EnvVar { + name: env_var_name, + id: block_id, + }); } else { error = error.or_else(|| { Some(ParseError::InternalError( @@ -945,12 +1008,9 @@ pub fn parse_export( *block_span, )) }); - None - }; + } call.add_positional(block_expr); - - exportable } else { let err_span = Span { start: name_span.end, @@ -964,8 +1024,6 @@ pub fn parse_export( call_signature, )) }); - - None } } else { let err_span = Span { @@ -980,20 +1038,20 @@ pub fn parse_export( call_signature, )) }); - - None } + + result } _ => { error = error.or_else(|| { Some(ParseError::Expected( // TODO: Fill in more keywords as they come - "def, def-env, alias, or env keyword".into(), + "def, def-env, alias, use, or env keyword".into(), spans[1], )) }); - None + vec![] } } } else { @@ -1008,7 +1066,7 @@ pub fn parse_export( )) }); - None + vec![] }; ( @@ -1018,7 +1076,7 @@ pub fn parse_export( ty: Type::Any, custom_completion: None, }]), - exportable, + exportables, error, ) } @@ -1085,6 +1143,15 @@ pub fn parse_module_block( (pipeline, err) } + b"use" => { + let (pipeline, _, err) = parse_use( + working_set, + &pipeline.commands[0].parts, + expand_aliases_denylist, + ); + + (pipeline, err) + } // TODO: Currently, it is not possible to define a private env var. // TODO: Exported env vars are usable iside the module only if correctly // exported by the user. For example: @@ -1094,28 +1161,25 @@ pub fn parse_module_block( // will work only if you call `use foo *; b` but not with `use foo; foo b` // since in the second case, the name of the env var would be $env."foo a". b"export" => { - let (pipe, exportable, err) = parse_export( + let (pipe, exportables, err) = parse_export( working_set, &pipeline.commands[0], expand_aliases_denylist, ); if err.is_none() { - let name_span = pipeline.commands[0].parts[2]; - let name = working_set.get_span_contents(name_span); - let name = trim_quotes(name); - - match exportable { - Some(Exportable::Decl(decl_id)) => { - module.add_decl(name, decl_id); + for exportable in exportables { + match exportable { + Exportable::Decl { name, id } => { + module.add_decl(name, id); + } + Exportable::Alias { name, id } => { + module.add_alias(name, id); + } + Exportable::EnvVar { name, id } => { + module.add_env_var(name, id); + } } - Some(Exportable::EnvVar(block_id)) => { - module.add_env_var(name, block_id); - } - Some(Exportable::Alias(alias_id)) => { - module.add_alias(name, alias_id); - } - None => {} // None should always come with error from parse_export() } } @@ -1242,10 +1306,11 @@ pub fn parse_use( working_set: &mut StateWorkingSet, spans: &[Span], expand_aliases_denylist: &[usize], -) -> (Pipeline, Option) { +) -> (Pipeline, Vec, Option) { if working_set.get_span_contents(spans[0]) != b"use" { return ( garbage_pipeline(spans), + vec![], Some(ParseError::UnknownState( "internal error: Wrong call name for 'use' command".into(), span(spans), @@ -1279,6 +1344,7 @@ pub fn parse_use( ty: output, custom_completion: None, }]), + vec![], err, ); } @@ -1288,6 +1354,7 @@ pub fn parse_use( None => { return ( garbage_pipeline(spans), + vec![], Some(ParseError::UnknownState( "internal error: 'use' declaration not found".into(), span(spans), @@ -1302,6 +1369,7 @@ pub fn parse_use( } else { return ( garbage_pipeline(spans), + vec![], Some(ParseError::UnknownState( "internal error: Import pattern positional is not import pattern".into(), expr.span, @@ -1311,6 +1379,7 @@ pub fn parse_use( } else { return ( garbage_pipeline(spans), + vec![], Some(ParseError::UnknownState( "internal error: Missing required positional after call parsing".into(), call_span, @@ -1348,6 +1417,7 @@ pub fn parse_use( ty: Type::Any, custom_completion: None, }]), + vec![], Some(ParseError::ModuleNotFound(spans[1])), ); }; @@ -1401,6 +1471,7 @@ pub fn parse_use( ty: Type::Any, custom_completion: None, }]), + vec![], Some(ParseError::ModuleNotFound(spans[1])), ); } @@ -1413,7 +1484,11 @@ pub fn parse_use( (import_pattern, Module::new()) } } else { - return (garbage_pipeline(spans), Some(ParseError::NonUtf8(spans[1]))); + return ( + garbage_pipeline(spans), + vec![], + Some(ParseError::NonUtf8(spans[1])), + ); } }; @@ -1459,6 +1534,22 @@ pub fn parse_use( } }; + let exportables = decls_to_use + .iter() + .map(|(name, decl_id)| Exportable::Decl { + name: name.clone(), + id: *decl_id, + }) + .chain( + aliases_to_use + .iter() + .map(|(name, alias_id)| Exportable::Alias { + name: name.clone(), + id: *alias_id, + }), + ) + .collect(); + // Extend the current scope with the module's exportables working_set.use_decls(decls_to_use); working_set.use_aliases(aliases_to_use); @@ -1486,6 +1577,7 @@ pub fn parse_use( ty: Type::Any, custom_completion: None, }]), + exportables, error, ) } @@ -1588,13 +1680,13 @@ pub fn parse_hide( if let Some(id) = working_set.find_alias(&import_pattern.head.name) { // an alias, let mut module = Module::new(); - module.add_alias(&import_pattern.head.name, id); + module.add_alias(import_pattern.head.name.clone(), id); (false, module) } else if let Some(id) = working_set.find_decl(&import_pattern.head.name, &Type::Any) { // a custom command, let mut module = Module::new(); - module.add_decl(&import_pattern.head.name, id); + module.add_decl(import_pattern.head.name.clone(), id); (false, module) } else { diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index 4353119520..3251da57a3 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -2812,9 +2812,9 @@ pub fn parse_import_pattern( expr: Expr::List(list), .. } => { - for l in list { - let contents = working_set.get_span_contents(l.span); - output.push((contents.to_vec(), l.span)); + for expr in list { + let contents = working_set.get_span_contents(expr.span); + output.push((trim_quotes(contents).to_vec(), expr.span)); } ( @@ -4786,7 +4786,11 @@ pub fn parse_builtin_commands( } b"alias" => parse_alias(working_set, &lite_command.parts, expand_aliases_denylist), b"module" => parse_module(working_set, &lite_command.parts, expand_aliases_denylist), - b"use" => parse_use(working_set, &lite_command.parts, expand_aliases_denylist), + b"use" => { + let (pipeline, _, err) = + parse_use(working_set, &lite_command.parts, expand_aliases_denylist); + (pipeline, err) + } b"overlay" => parse_overlay(working_set, &lite_command.parts, expand_aliases_denylist), b"source" => parse_source(working_set, &lite_command.parts, expand_aliases_denylist), b"export" => { diff --git a/crates/nu-protocol/src/exportable.rs b/crates/nu-protocol/src/exportable.rs index bc472d71c5..808539e2e9 100644 --- a/crates/nu-protocol/src/exportable.rs +++ b/crates/nu-protocol/src/exportable.rs @@ -1,7 +1,7 @@ use crate::{AliasId, BlockId, DeclId}; pub enum Exportable { - Decl(DeclId), - Alias(AliasId), - EnvVar(BlockId), + Decl { name: Vec, id: DeclId }, + Alias { name: Vec, id: AliasId }, + EnvVar { name: Vec, id: BlockId }, } diff --git a/crates/nu-protocol/src/module.rs b/crates/nu-protocol/src/module.rs index 0479e0bca3..11fe363d49 100644 --- a/crates/nu-protocol/src/module.rs +++ b/crates/nu-protocol/src/module.rs @@ -33,16 +33,16 @@ impl Module { } } - pub fn add_decl(&mut self, name: &[u8], decl_id: DeclId) -> Option { - self.decls.insert(name.to_vec(), decl_id) + pub fn add_decl(&mut self, name: Vec, decl_id: DeclId) -> Option { + self.decls.insert(name, decl_id) } - pub fn add_alias(&mut self, name: &[u8], alias_id: AliasId) -> Option { - self.aliases.insert(name.to_vec(), alias_id) + pub fn add_alias(&mut self, name: Vec, alias_id: AliasId) -> Option { + self.aliases.insert(name, alias_id) } - pub fn add_env_var(&mut self, name: &[u8], block_id: BlockId) -> Option { - self.env_vars.insert(name.to_vec(), block_id) + pub fn add_env_var(&mut self, name: Vec, block_id: BlockId) -> Option { + self.env_vars.insert(name, block_id) } pub fn extend(&mut self, other: &Module) { diff --git a/tests/main.rs b/tests/main.rs index 004beaa7c4..a74971e0ad 100644 --- a/tests/main.rs +++ b/tests/main.rs @@ -1,6 +1,7 @@ extern crate nu_test_support; mod hooks; +mod modules; mod nu_repl; mod overlays; mod parsing; diff --git a/tests/modules/mod.rs b/tests/modules/mod.rs new file mode 100644 index 0000000000..e65c07e982 --- /dev/null +++ b/tests/modules/mod.rs @@ -0,0 +1,347 @@ +use nu_test_support::fs::Stub::FileWithContentToBeTrimmed; +use nu_test_support::playground::Playground; +use nu_test_support::{nu, pipeline}; + +#[test] +fn module_private_import_decl() { + Playground::setup("module_private_import_decl", |dirs, sandbox| { + sandbox + .with_files(vec![FileWithContentToBeTrimmed( + "main.nu", + r#" + use spam.nu foo-helper + + export def foo [] { foo-helper } + "#, + )]) + .with_files(vec![FileWithContentToBeTrimmed( + "spam.nu", + r#" + def get-foo [] { "foo" } + export def foo-helper [] { get-foo } + "#, + )]); + + let inp = &[r#"use main.nu foo"#, r#"foo"#]; + + let actual = nu!(cwd: dirs.test(), pipeline(&inp.join("; "))); + + assert_eq!(actual.out, "foo"); + }) +} + +#[test] +fn module_private_import_alias() { + Playground::setup("module_private_import_alias", |dirs, sandbox| { + sandbox + .with_files(vec![FileWithContentToBeTrimmed( + "main.nu", + r#" + use spam.nu foo-helper + + export def foo [] { foo-helper } + "#, + )]) + .with_files(vec![FileWithContentToBeTrimmed( + "spam.nu", + r#" + export alias foo-helper = "foo" + "#, + )]); + + let inp = &[r#"use main.nu foo"#, r#"foo"#]; + + let actual = nu!(cwd: dirs.test(), pipeline(&inp.join("; "))); + + assert_eq!(actual.out, "foo"); + }) +} + +#[test] +fn module_private_import_decl_not_public() { + Playground::setup("module_private_import_decl_not_public", |dirs, sandbox| { + sandbox + .with_files(vec![FileWithContentToBeTrimmed( + "main.nu", + r#" + use spam.nu foo-helper + "#, + )]) + .with_files(vec![FileWithContentToBeTrimmed( + "spam.nu", + r#" + def get-foo [] { "foo" } + export def foo-helper [] { get-foo } + "#, + )]); + + let inp = &[r#"use main.nu foo"#, r#"foo-helper"#]; + + let actual = nu!(cwd: dirs.test(), pipeline(&inp.join("; "))); + + assert!(!actual.err.is_empty()); + }) +} + +// TODO -- doesn't work because modules are never evaluated +#[ignore] +#[test] +fn module_private_import_env() { + Playground::setup("module_private_import_env", |dirs, sandbox| { + sandbox + .with_files(vec![FileWithContentToBeTrimmed( + "main.nu", + r#" + use spam.nu FOO_HELPER + + export def foo [] { $env.FOO_HELPER } + "#, + )]) + .with_files(vec![FileWithContentToBeTrimmed( + "spam.nu", + r#" + export env FOO_HELPER { "foo" } + "#, + )]); + + let inp = &[r#"use main.nu foo"#, r#"foo"#]; + + let actual = nu!(cwd: dirs.test(), pipeline(&inp.join("; "))); + + assert_eq!(actual.out, "foo"); + }) +} + +#[test] +fn module_public_import_decl() { + Playground::setup("module_public_import_decl", |dirs, sandbox| { + sandbox + .with_files(vec![FileWithContentToBeTrimmed( + "main.nu", + r#" + export use spam.nu foo + "#, + )]) + .with_files(vec![FileWithContentToBeTrimmed( + "spam.nu", + r#" + def foo-helper [] { "foo" } + export def foo [] { foo-helper } + "#, + )]); + + let inp = &[r#"use main.nu foo"#, r#"foo"#]; + + let actual = nu!(cwd: dirs.test(), pipeline(&inp.join("; "))); + + assert_eq!(actual.out, "foo"); + }) +} + +#[test] +fn module_public_import_alias() { + Playground::setup("module_public_import_alias", |dirs, sandbox| { + sandbox + .with_files(vec![FileWithContentToBeTrimmed( + "main.nu", + r#" + export use spam.nu foo + "#, + )]) + .with_files(vec![FileWithContentToBeTrimmed( + "spam.nu", + r#" + export alias foo = "foo" + "#, + )]); + + let inp = &[r#"use main.nu foo"#, r#"foo"#]; + + let actual = nu!(cwd: dirs.test(), pipeline(&inp.join("; "))); + + assert_eq!(actual.out, "foo"); + }) +} + +// TODO -- doesn't work because modules are never evaluated +#[ignore] +#[test] +fn module_public_import_env() { + Playground::setup("module_public_import_decl", |dirs, sandbox| { + sandbox + .with_files(vec![FileWithContentToBeTrimmed( + "main.nu", + r#" + export use spam.nu FOO + "#, + )]) + .with_files(vec![FileWithContentToBeTrimmed( + "spam.nu", + r#" + export env FOO { "foo" } + "#, + )]); + + let inp = &[r#"use main.nu FOO"#, r#"$env.FOO"#]; + + let actual = nu!(cwd: dirs.test(), pipeline(&inp.join("; "))); + + assert_eq!(actual.out, "foo"); + }) +} + +#[test] +fn module_nested_imports() { + Playground::setup("module_nested_imports", |dirs, sandbox| { + sandbox + .with_files(vec![FileWithContentToBeTrimmed( + "main.nu", + r#" + export use spam.nu [ foo bar ] + "#, + )]) + .with_files(vec![FileWithContentToBeTrimmed( + "spam.nu", + r#" + export use spam2.nu [ foo bar ] + "#, + )]) + .with_files(vec![FileWithContentToBeTrimmed( + "spam2.nu", + r#" + export use spam3.nu [ foo bar ] + "#, + )]) + .with_files(vec![FileWithContentToBeTrimmed( + "spam3.nu", + r#" + export def foo [] { "foo" } + export alias bar = "bar" + "#, + )]); + + let inp1 = &[r#"use main.nu foo"#, r#"foo"#]; + let inp2 = &[r#"use main.nu bar"#, r#"bar"#]; + + let actual = nu!(cwd: dirs.test(), pipeline(&inp1.join("; "))); + assert_eq!(actual.out, "foo"); + + let actual = nu!(cwd: dirs.test(), pipeline(&inp2.join("; "))); + assert_eq!(actual.out, "bar"); + }) +} + +#[test] +fn module_nested_imports_in_dirs() { + Playground::setup("module_nested_imports_in_dirs", |dirs, sandbox| { + sandbox + .mkdir("spam") + .mkdir("spam/spam2") + .mkdir("spam/spam3") + .with_files(vec![FileWithContentToBeTrimmed( + "main.nu", + r#" + export use spam/spam.nu [ foo bar ] + "#, + )]) + .with_files(vec![FileWithContentToBeTrimmed( + "spam/spam.nu", + r#" + export use spam2/spam2.nu [ foo bar ] + "#, + )]) + .with_files(vec![FileWithContentToBeTrimmed( + "spam/spam2/spam2.nu", + r#" + export use ../spam3/spam3.nu [ foo bar ] + "#, + )]) + .with_files(vec![FileWithContentToBeTrimmed( + "spam/spam3/spam3.nu", + r#" + export def foo [] { "foo" } + export alias bar = "bar" + "#, + )]); + + let inp1 = &[r#"use main.nu foo"#, r#"foo"#]; + let inp2 = &[r#"use main.nu bar"#, r#"bar"#]; + + let actual = nu!(cwd: dirs.test(), pipeline(&inp1.join("; "))); + assert_eq!(actual.out, "foo"); + + let actual = nu!(cwd: dirs.test(), pipeline(&inp2.join("; "))); + assert_eq!(actual.out, "bar"); + }) +} + +#[test] +fn module_public_import_decl_prefixed() { + Playground::setup("module_public_import_decl", |dirs, sandbox| { + sandbox + .with_files(vec![FileWithContentToBeTrimmed( + "main.nu", + r#" + export use spam.nu + "#, + )]) + .with_files(vec![FileWithContentToBeTrimmed( + "spam.nu", + r#" + def foo-helper [] { "foo" } + export def foo [] { foo-helper } + "#, + )]); + + let inp = &[r#"use main.nu"#, r#"main spam foo"#]; + + let actual = nu!(cwd: dirs.test(), pipeline(&inp.join("; "))); + + assert_eq!(actual.out, "foo"); + }) +} + +#[test] +fn module_nested_imports_in_dirs_prefixed() { + Playground::setup("module_nested_imports_in_dirs", |dirs, sandbox| { + sandbox + .mkdir("spam") + .mkdir("spam/spam2") + .mkdir("spam/spam3") + .with_files(vec![FileWithContentToBeTrimmed( + "main.nu", + r#" + export use spam/spam.nu [ "spam2 foo" "spam2 spam3 bar" ] + "#, + )]) + .with_files(vec![FileWithContentToBeTrimmed( + "spam/spam.nu", + r#" + export use spam2/spam2.nu + "#, + )]) + .with_files(vec![FileWithContentToBeTrimmed( + "spam/spam2/spam2.nu", + r#" + export use ../spam3/spam3.nu + export use ../spam3/spam3.nu foo + "#, + )]) + .with_files(vec![FileWithContentToBeTrimmed( + "spam/spam3/spam3.nu", + r#" + export def foo [] { "foo" } + export alias bar = "bar" + "#, + )]); + + let inp1 = &[r#"use main.nu"#, r#"main spam2 foo"#]; + let inp2 = &[r#"use main.nu "spam2 spam3 bar""#, r#"spam2 spam3 bar"#]; + + let actual = nu!(cwd: dirs.test(), pipeline(&inp1.join("; "))); + assert_eq!(actual.out, "foo"); + + let actual = nu!(cwd: dirs.test(), pipeline(&inp2.join("; "))); + assert_eq!(actual.out, "bar"); + }) +}