diff --git a/crates/nu-cmd-lang/src/core_commands/export_module.rs b/crates/nu-cmd-lang/src/core_commands/export_module.rs new file mode 100644 index 0000000000..788a4e85fe --- /dev/null +++ b/crates/nu-cmd-lang/src/core_commands/export_module.rs @@ -0,0 +1,75 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, PipelineData, ShellError, Signature, SyntaxShape, Type, Value, +}; + +#[derive(Clone)] +pub struct ExportModule; + +impl Command for ExportModule { + fn name(&self) -> &str { + "export module" + } + + fn usage(&self) -> &str { + "Export a custom module from a module." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("export module") + .input_output_types(vec![(Type::Nothing, Type::Nothing)]) + .allow_variants_without_examples(true) + .required("module", SyntaxShape::String, "module name or module path") + .optional( + "block", + SyntaxShape::Block, + "body of the module if 'module' parameter is not a path", + ) + .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_nu.html"# + } + + fn is_parser_keyword(&self) -> bool { + true + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + _input: PipelineData, + ) -> Result { + Ok(PipelineData::empty()) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Define a custom command in a submodule of a module and call it", + example: r#"module spam { + export module eggs { + export def foo [] { "foo" } + } + } + use spam eggs + eggs foo"#, + result: Some(Value::test_string("foo")), + }] + } +} + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(ExportModule {}) + } +} diff --git a/crates/nu-cmd-lang/src/core_commands/mod.rs b/crates/nu-cmd-lang/src/core_commands/mod.rs index 4bc4ba8a5e..efde1a4651 100644 --- a/crates/nu-cmd-lang/src/core_commands/mod.rs +++ b/crates/nu-cmd-lang/src/core_commands/mod.rs @@ -14,6 +14,7 @@ mod export_alias; mod export_def; mod export_def_env; mod export_extern; +mod export_module; mod export_use; mod extern_; mod for_; @@ -55,6 +56,7 @@ pub use export_alias::ExportAlias; pub use export_def::ExportDef; pub use export_def_env::ExportDefEnv; pub use export_extern::ExportExtern; +pub use export_module::ExportModule; pub use export_use::ExportUse; pub use extern_::Extern; pub use for_::For; diff --git a/crates/nu-cmd-lang/src/core_commands/module.rs b/crates/nu-cmd-lang/src/core_commands/module.rs index 74ebc98473..8bc3ece6b5 100644 --- a/crates/nu-cmd-lang/src/core_commands/module.rs +++ b/crates/nu-cmd-lang/src/core_commands/module.rs @@ -19,8 +19,13 @@ impl Command for Module { fn signature(&self) -> nu_protocol::Signature { Signature::build("module") .input_output_types(vec![(Type::Nothing, Type::Nothing)]) - .required("module_name", SyntaxShape::String, "module name") - .required("block", SyntaxShape::Block, "body of the module") + .allow_variants_without_examples(true) + .required("module", SyntaxShape::String, "module name or module path") + .optional( + "block", + SyntaxShape::Block, + "body of the module if 'module' parameter is not a module path", + ) .category(Category::Core) } diff --git a/crates/nu-cmd-lang/src/core_commands/use_.rs b/crates/nu-cmd-lang/src/core_commands/use_.rs index 67e8b814c7..41f4c4893a 100644 --- a/crates/nu-cmd-lang/src/core_commands/use_.rs +++ b/crates/nu-cmd-lang/src/core_commands/use_.rs @@ -21,7 +21,7 @@ impl Command for Use { Signature::build("use") .input_output_types(vec![(Type::Nothing, Type::Nothing)]) .required("module", SyntaxShape::String, "Module or module file") - .optional( + .rest( "members", SyntaxShape::Any, "Which members of the module to import", diff --git a/crates/nu-cmd-lang/src/default_context.rs b/crates/nu-cmd-lang/src/default_context.rs index ca224da94d..31ebbc0b6f 100644 --- a/crates/nu-cmd-lang/src/default_context.rs +++ b/crates/nu-cmd-lang/src/default_context.rs @@ -33,6 +33,7 @@ pub fn create_default_context() -> EngineState { ExportDefEnv, ExportExtern, ExportUse, + ExportModule, Extern, For, Help, diff --git a/crates/nu-cmd-lang/src/example_test.rs b/crates/nu-cmd-lang/src/example_test.rs index c65c0963a7..0d1e13b1ab 100644 --- a/crates/nu-cmd-lang/src/example_test.rs +++ b/crates/nu-cmd-lang/src/example_test.rs @@ -13,11 +13,12 @@ mod test_examples { check_example_evaluates_to_expected_output, check_example_input_and_output_types_match_command_signature, }; - use crate::{Break, Collect, Describe, Mut}; - use crate::{Echo, If, Let}; + use crate::{ + Break, Collect, Def, Describe, Echo, ExportCommand, ExportDef, If, Let, Module, Mut, Use, + }; use nu_protocol::{ engine::{Command, EngineState, StateWorkingSet}, - Type, + Type, Value, }; use std::collections::HashSet; @@ -55,18 +56,28 @@ mod test_examples { fn make_engine_state(cmd: Box) -> Box { let mut engine_state = Box::new(EngineState::new()); + let cwd = std::env::current_dir() + .expect("Could not get current working directory.") + .to_string_lossy() + .to_string(); + engine_state.add_env_var("PWD".to_string(), Value::test_string(cwd)); let delta = { // Base functions that are needed for testing // Try to keep this working set small to keep tests running as fast as possible let mut working_set = StateWorkingSet::new(&engine_state); working_set.add_decl(Box::new(Break)); + working_set.add_decl(Box::new(Collect)); + working_set.add_decl(Box::new(Def)); working_set.add_decl(Box::new(Describe)); working_set.add_decl(Box::new(Echo)); + working_set.add_decl(Box::new(ExportCommand)); + working_set.add_decl(Box::new(ExportDef)); working_set.add_decl(Box::new(If)); working_set.add_decl(Box::new(Let)); + working_set.add_decl(Box::new(Module)); working_set.add_decl(Box::new(Mut)); - working_set.add_decl(Box::new(Collect)); + working_set.add_decl(Box::new(Use)); // Adding the command that is being tested to the working set working_set.add_decl(cmd); diff --git a/crates/nu-parser/src/parse_keywords.rs b/crates/nu-parser/src/parse_keywords.rs index d394bdf786..c644b5e868 100644 --- a/crates/nu-parser/src/parse_keywords.rs +++ b/crates/nu-parser/src/parse_keywords.rs @@ -6,10 +6,11 @@ use nu_protocol::{ ImportPatternMember, Pipeline, PipelineElement, }, engine::{StateWorkingSet, DEFAULT_OVERLAY_NAME}, - span, Alias, BlockId, Exportable, Module, ParseError, PositionalArg, Span, Spanned, - SyntaxShape, Type, VarId, + span, Alias, BlockId, Exportable, Module, ModuleId, ParseError, PositionalArg, + ResolvedImportPattern, Span, Spanned, SyntaxShape, Type, VarId, }; use std::collections::{HashMap, HashSet}; +use std::ffi::OsStr; use std::path::{Path, PathBuf}; pub const LIB_DIRS_VAR: &str = "NU_LIB_DIRS"; @@ -408,6 +409,7 @@ pub fn parse_def( working_set.error(ParseError::NamedAsModule( "command".to_string(), name, + "main".to_string(), name_expr_span, )); return Pipeline::from_vec(vec![Expression { @@ -530,6 +532,7 @@ pub fn parse_extern( working_set.error(ParseError::NamedAsModule( "known external".to_string(), name.clone(), + "main".to_string(), name_expr_span, )); return Pipeline::from_vec(vec![Expression { @@ -730,6 +733,7 @@ pub fn parse_alias( working_set.error(ParseError::NamedAsModule( "alias".to_string(), alias_name, + "main".to_string(), spans[split_id], )); @@ -861,7 +865,9 @@ pub fn parse_export_in_block( let full_name = if lite_command.parts.len() > 1 { let sub = working_set.get_span_contents(lite_command.parts[1]); match sub { - b"alias" | b"def" | b"def-env" | b"extern" | b"use" => [b"export ", sub].concat(), + b"alias" | b"def" | b"def-env" | b"extern" | b"use" | b"module" => { + [b"export ", sub].concat() + } _ => b"export".to_vec(), } } else { @@ -923,6 +929,7 @@ pub fn parse_export_in_block( let (pipeline, _) = parse_use(working_set, &lite_command.parts); pipeline } + b"export module" => parse_module(working_set, lite_command, None), b"export extern" => parse_extern(working_set, lite_command, None), _ => { working_set.error(ParseError::UnexpectedKeyword( @@ -961,7 +968,9 @@ pub fn parse_export_in_module( return (garbage_pipeline(spans), vec![]); }; - let Some(export_decl_id) = working_set.find_decl(b"export", &Type::Any) else { + let export_decl_id = if let Some(id) = working_set.find_decl(b"export", &Type::Any) { + id + } else { working_set.error(ParseError::InternalError( "missing export command".into(), export_span, @@ -1260,10 +1269,64 @@ pub fn parse_export_in_module( exportables } + b"module" => { + let pipeline = parse_module(working_set, lite_command, Some(module_name)); + + let export_module_decl_id = + if let Some(id) = working_set.find_decl(b"export module", &Type::Any) { + id + } else { + working_set.error(ParseError::InternalError( + "missing 'export module' command".into(), + export_span, + )); + return (garbage_pipeline(spans), vec![]); + }; + + // Trying to warp the 'module' call into the 'export module' in a very clumsy way + if let Some(PipelineElement::Expression( + _, + Expression { + expr: Expr::Call(ref module_call), + .. + }, + )) = pipeline.elements.get(0) + { + call = module_call.clone(); + + call.head = span(&spans[0..=1]); + call.decl_id = export_module_decl_id; + } else { + working_set.error(ParseError::InternalError( + "unexpected output from parsing a definition".into(), + span(&spans[1..]), + )); + }; + + let mut result = vec![]; + + if let Some(module_name_span) = spans.get(2) { + let module_name = working_set.get_span_contents(*module_name_span); + let module_name = trim_quotes(module_name); + + if let Some(module_id) = working_set.find_module(module_name) { + result.push(Exportable::Module { + name: module_name.to_vec(), + id: module_id, + }); + } else { + working_set.error(ParseError::InternalError( + "failed to find added module".into(), + span(&spans[1..]), + )); + } + } + + result + } _ => { working_set.error(ParseError::Expected( - // TODO: Fill in more keywords as they come - "def, def-env, alias, use, or extern keyword".into(), + "def, def-env, alias, use, module, or extern keyword".into(), spans[1], )); @@ -1272,9 +1335,9 @@ pub fn parse_export_in_module( } } else { working_set.error(ParseError::MissingPositional( - "def, def-env, alias, use, or extern keyword".into(), // TODO: keep filling more keywords as they come + "def, def-env, extern, alias, use, or module keyword".into(), Span::new(export_span.end, export_span.end), - "`def`, `def-env`, `alias`, use, or `extern` keyword.".to_string(), + "def, def-env, extern, alias, use, or module keyword.".to_string(), )); vec![] @@ -1465,6 +1528,15 @@ pub fn parse_module_block( block.pipelines.push(pipeline) } + b"module" => { + let pipeline = parse_module( + working_set, + command, + None, // using modules named as the module locally is OK + ); + + block.pipelines.push(pipeline) + } b"export" => { let (pipe, exportables) = parse_export_in_module(working_set, command, module_name); @@ -1473,11 +1545,96 @@ pub fn parse_module_block( match exportable { Exportable::Decl { name, id } => { if &name == b"main" { - module.main = Some(id); + if module.main.is_some() { + let err_span = if !pipe.elements.is_empty() { + if let PipelineElement::Expression( + _, + Expression { + expr: Expr::Call(call), + .. + }, + ) = &pipe.elements[0] + { + call.head + } else { + pipe.elements[0].span() + } + } else { + span + }; + working_set.error(ParseError::ModuleDoubleMain( + String::from_utf8_lossy(module_name) + .to_string(), + err_span, + )); + } else { + module.main = Some(id); + } } else { module.add_decl(name, id); } } + Exportable::Module { name, id } => { + if &name == b"mod" { + let ( + submodule_main, + submodule_decls, + submodule_submodules, + ) = { + let submodule = working_set.get_module(id); + ( + submodule.main, + submodule.decls(), + submodule.submodules(), + ) + }; + + // Add submodule's decls to the parent module + for (decl_name, decl_id) in submodule_decls { + module.add_decl(decl_name, decl_id); + } + + // Add submodule's main command to the parent module + if let Some(main_decl_id) = submodule_main { + if module.main.is_some() { + let err_span = if !pipe.elements.is_empty() { + if let PipelineElement::Expression( + _, + Expression { + expr: Expr::Call(call), + .. + }, + ) = &pipe.elements[0] + { + call.head + } else { + pipe.elements[0].span() + } + } else { + span + }; + working_set.error( + ParseError::ModuleDoubleMain( + String::from_utf8_lossy(module_name) + .to_string(), + err_span, + ), + ); + } else { + module.main = Some(main_decl_id); + } + } + + // Add submodule's submodules to the parent module + for (submodule_name, submodule_id) in + submodule_submodules + { + module.add_submodule(submodule_name, submodule_id); + } + } else { + module.add_submodule(name, id); + } + } } } @@ -1495,7 +1652,7 @@ pub fn parse_module_block( } _ => { working_set.error(ParseError::ExpectedKeyword( - "def or export keyword".into(), + "def, def-env, extern, alias, use, module, export or export-env keyword".into(), command.parts[0], )); @@ -1521,87 +1678,368 @@ pub fn parse_module_block( (block, module, module_comments) } -pub fn parse_module(working_set: &mut StateWorkingSet, lite_command: &LiteCommand) -> Pipeline { +fn parse_module_file( + working_set: &mut StateWorkingSet, + path: PathBuf, + path_span: Span, + name_override: Option, +) -> Option { + if let Some(i) = working_set + .parsed_module_files + .iter() + .rposition(|p| p == &path) + { + let mut files: Vec = working_set + .parsed_module_files + .split_off(i) + .iter() + .map(|p| p.to_string_lossy().to_string()) + .collect(); + + files.push(path.to_string_lossy().to_string()); + + let msg = files.join("\nuses "); + + working_set.error(ParseError::CyclicalModuleImport(msg, path_span)); + return None; + } + + let module_name = if let Some(name) = name_override { + name + } else if let Some(stem) = path.file_stem() { + stem.to_string_lossy().to_string() + } else { + working_set.error(ParseError::ModuleNotFound(path_span)); + return None; + }; + + let contents = if let Ok(contents) = std::fs::read(&path) { + contents + } else { + working_set.error(ParseError::ModuleNotFound(path_span)); + return None; + }; + + let file_id = working_set.add_file(path.to_string_lossy().to_string(), &contents); + let new_span = working_set.get_span_for_file(file_id); + + // Change the currently parsed directory + let prev_currently_parsed_cwd = if let Some(parent) = path.parent() { + let prev = working_set.currently_parsed_cwd.clone(); + + working_set.currently_parsed_cwd = Some(parent.into()); + + prev + } else { + working_set.currently_parsed_cwd.clone() + }; + + // Add the file to the stack of parsed module files + working_set.parsed_module_files.push(path); + + // Parse the module + let (block, module, module_comments) = + parse_module_block(working_set, new_span, module_name.as_bytes()); + + // Remove the file from the stack of parsed module files + working_set.parsed_module_files.pop(); + + // Restore the currently parsed directory back + working_set.currently_parsed_cwd = prev_currently_parsed_cwd; + + let _ = working_set.add_block(block); + let module_id = working_set.add_module(&module_name, module, module_comments); + + Some(module_id) +} + +pub fn parse_module_file_or_dir( + working_set: &mut StateWorkingSet, + path: &[u8], + path_span: Span, + name_override: Option, +) -> Option { + let (module_path_str, err) = unescape_unquote_string(path, path_span); + if let Some(err) = err { + working_set.error(err); + return None; + } + + let cwd = working_set.get_cwd(); + + let module_path = + if let Some(path) = find_in_dirs(&module_path_str, working_set, &cwd, LIB_DIRS_VAR) { + path + } else { + working_set.error(ParseError::ModuleNotFound(path_span)); + return None; + }; + + if module_path.is_dir() { + if let Ok(dir_contents) = std::fs::read_dir(&module_path) { + let module_name = if let Some(stem) = module_path.file_stem() { + stem.to_string_lossy().to_string() + } else { + working_set.error(ParseError::ModuleNotFound(path_span)); + return None; + }; + + let mut file_paths = vec![]; + + for entry in dir_contents.flatten() { + let entry_path = entry.path(); + + if entry_path.is_file() + && entry_path.extension() == Some(OsStr::new("nu")) + && entry_path.file_stem() != Some(OsStr::new("mod")) + { + if entry_path.file_stem() == Some(OsStr::new(&module_name)) { + working_set.error(ParseError::InvalidModuleFileName( + module_path.to_string_lossy().to_string(), + module_name, + path_span, + )); + return None; + } + + file_paths.push(entry_path); + } + } + + file_paths.sort(); + + // working_set.enter_scope(); + + let mut submodules = vec![]; + + for file_path in file_paths { + if let Some(submodule_id) = + parse_module_file(working_set, file_path, path_span, None) + { + let submodule_name = working_set.get_module(submodule_id).name(); + submodules.push((submodule_name, submodule_id)); + } + } + + let mod_nu_path = module_path.join("mod.nu"); + + if mod_nu_path.exists() && mod_nu_path.is_file() { + if let Some(module_id) = parse_module_file( + working_set, + mod_nu_path, + path_span, + name_override.or(Some(module_name)), + ) { + let module = working_set.get_module_mut(module_id); + + for (submodule_name, submodule_id) in submodules { + module.add_submodule(submodule_name, submodule_id); + } + + Some(module_id) + } else { + None + } + } else { + let mut module = Module::new(module_name.as_bytes().to_vec()); + + for (submodule_name, submodule_id) in submodules { + module.add_submodule(submodule_name, submodule_id); + } + + Some(working_set.add_module(&module_name, module, vec![])) + } + } else { + working_set.error(ParseError::ModuleNotFound(path_span)); + None + } + } else if module_path.is_file() { + parse_module_file(working_set, module_path, path_span, name_override) + } else { + working_set.error(ParseError::ModuleNotFound(path_span)); + None + } +} + +pub fn parse_module( + working_set: &mut StateWorkingSet, + lite_command: &LiteCommand, + module_name: Option<&[u8]>, +) -> Pipeline { // TODO: Currently, module is closing over its parent scope (i.e., defs in the parent scope are // visible and usable in this module's scope). We want to disable that for files. let spans = &lite_command.parts; let mut module_comments = lite_command.comments.clone(); - let bytes = working_set.get_span_contents(spans[0]); + let split_id = if spans.len() > 1 && working_set.get_span_contents(spans[0]) == b"export" { + 2 + } else { + 1 + }; - if bytes == b"module" && spans.len() >= 3 { - let module_name_expr = parse_string(working_set, spans[1]); + let (call, call_span) = match working_set.find_decl(b"module", &Type::Any) { + Some(decl_id) => { + let (command_spans, rest_spans) = spans.split_at(split_id); - let module_name = module_name_expr - .as_string() - .expect("internal error: module name is not a string"); + let ParsedInternalCall { call, output } = + parse_internal_call(working_set, span(command_spans), rest_spans, decl_id); + let decl = working_set.get_decl(decl_id); - let block_span = spans[2]; - let block_bytes = working_set.get_span_contents(block_span); - let mut start = block_span.start; - let mut end = block_span.end; + let call_span = span(spans); - if block_bytes.starts_with(b"{") { - start += 1; - } else { - working_set.error(ParseError::Expected("block".into(), block_span)); + let starting_error_count = working_set.parse_errors.len(); + check_call(working_set, call_span, &decl.signature(), &call); + if starting_error_count != working_set.parse_errors.len() || call.has_flag("help") { + return Pipeline::from_vec(vec![Expression { + expr: Expr::Call(call), + span: call_span, + ty: output, + custom_completion: None, + }]); + } + + (call, call_span) + } + None => { + working_set.error(ParseError::UnknownState( + "internal error: 'module' or 'export module' declaration not found".into(), + span(spans), + )); return garbage_pipeline(spans); } + }; - if block_bytes.ends_with(b"}") { - end -= 1; + let (module_name_or_path, module_name_or_path_span, module_name_or_path_expr) = + if let Some(name) = call.positional_nth(0) { + if let Some(s) = name.as_string() { + if let Some(mod_name) = module_name { + if s.as_bytes() == mod_name { + working_set.error(ParseError::NamedAsModule( + "module".to_string(), + s, + "mod".to_string(), + name.span, + )); + return Pipeline::from_vec(vec![Expression { + expr: Expr::Call(call), + span: call_span, + ty: Type::Any, + custom_completion: None, + }]); + } + } + (s, name.span, name.clone()) + } else { + working_set.error(ParseError::UnknownState( + "internal error: name not a string".into(), + span(spans), + )); + return garbage_pipeline(spans); + } } else { - working_set.error(ParseError::Unclosed("}".into(), Span::new(end, end))); - } - - let block_span = Span::new(start, end); - - let (block, module, inner_comments) = - parse_module_block(working_set, block_span, module_name.as_bytes()); - - let block_id = working_set.add_block(block); - - module_comments.extend(inner_comments); - let _ = working_set.add_module(&module_name, module, module_comments); - - let block_expr = Expression { - expr: Expr::Block(block_id), - span: block_span, - ty: Type::Block, - custom_completion: None, + working_set.error(ParseError::UnknownState( + "internal error: missing positional".into(), + span(spans), + )); + return garbage_pipeline(spans); }; - let module_decl_id = working_set - .find_decl(b"module", &Type::Any) - .expect("internal error: missing module command"); + let pipeline = Pipeline::from_vec(vec![Expression { + expr: Expr::Call(call), + span: call_span, + ty: Type::Any, + custom_completion: None, + }]); - let call = Box::new(Call { - head: spans[0], - decl_id: module_decl_id, - arguments: vec![ - Argument::Positional(module_name_expr), - Argument::Positional(block_expr), - ], - redirect_stdout: true, - redirect_stderr: false, - parser_info: HashMap::new(), - }); + if spans.len() == split_id + 1 { + let cwd = working_set.get_cwd(); - Pipeline::from_vec(vec![Expression { - expr: Expr::Call(call), - span: span(spans), - ty: Type::Any, - custom_completion: None, - }]) - } else { + if let Some(module_path) = + find_in_dirs(&module_name_or_path, working_set, &cwd, LIB_DIRS_VAR) + { + let path_str = module_path.to_string_lossy().to_string(); + let _ = parse_module_file_or_dir( + working_set, + path_str.as_bytes(), + module_name_or_path_span, + None, + ); + return pipeline; + } else { + working_set.error(ParseError::ModuleNotFound(module_name_or_path_span)); + return pipeline; + } + } + + if spans.len() < split_id + 2 { working_set.error(ParseError::UnknownState( - "Expected structure: module {}".into(), + "Expected structure: module or module ".into(), span(spans), )); - garbage_pipeline(spans) + return garbage_pipeline(spans); } + + let module_name = module_name_or_path; + + let block_span = spans[split_id + 1]; + let block_bytes = working_set.get_span_contents(block_span); + let mut start = block_span.start; + let mut end = block_span.end; + + if block_bytes.starts_with(b"{") { + start += 1; + } else { + working_set.error(ParseError::Expected("block".into(), block_span)); + return garbage_pipeline(spans); + } + + if block_bytes.ends_with(b"}") { + end -= 1; + } else { + working_set.error(ParseError::Unclosed("}".into(), Span::new(end, end))); + } + + let block_span = Span::new(start, end); + + let (block, module, inner_comments) = + parse_module_block(working_set, block_span, module_name.as_bytes()); + + let block_id = working_set.add_block(block); + + module_comments.extend(inner_comments); + let _ = working_set.add_module(&module_name, module, module_comments); + + let block_expr = Expression { + expr: Expr::Block(block_id), + span: block_span, + ty: Type::Block, + custom_completion: None, + }; + + let module_decl_id = working_set + .find_decl(b"module", &Type::Any) + .expect("internal error: missing module command"); + + let call = Box::new(Call { + head: span(&spans[..split_id]), + decl_id: module_decl_id, + arguments: vec![ + Argument::Positional(module_name_or_path_expr), + Argument::Positional(block_expr), + ], + redirect_stdout: true, + redirect_stderr: false, + parser_info: HashMap::new(), + }); + + Pipeline::from_vec(vec![Expression { + expr: Expr::Call(call), + span: span(spans), + ty: Type::Any, + custom_completion: None, + }]) } pub fn parse_use(working_set: &mut StateWorkingSet, spans: &[Span]) -> (Pipeline, Vec) { @@ -1680,205 +2118,81 @@ pub fn parse_use(working_set: &mut StateWorkingSet, spans: &[Span]) -> (Pipeline return (garbage_pipeline(spans), vec![]); }; - let cwd = working_set.get_cwd(); - - // TODO: Add checking for importing too long import patterns, e.g.: - // > use spam foo non existent names here do not throw error - let (import_pattern, module) = if let Some(module_id) = import_pattern.head.id { - (import_pattern, working_set.get_module(module_id).clone()) + let (import_pattern, module, module_id) = if let Some(module_id) = import_pattern.head.id { + let module = working_set.get_module(module_id).clone(); + ( + ImportPattern { + head: ImportPatternHead { + name: module.name.clone(), + id: Some(module_id), + span: import_pattern.head.span, + }, + members: import_pattern.members, + hidden: HashSet::new(), + }, + module, + module_id, + ) + } else if let Some(module_id) = parse_module_file_or_dir( + working_set, + &import_pattern.head.name, + import_pattern.head.span, + None, + ) { + let module = working_set.get_module(module_id).clone(); + ( + ImportPattern { + head: ImportPatternHead { + name: module.name.clone(), + id: Some(module_id), + span: import_pattern.head.span, + }, + members: import_pattern.members, + hidden: HashSet::new(), + }, + module, + module_id, + ) } else { - // It could be a file - // TODO: Do not close over when loading module from file? - - let starting_error_count = working_set.parse_errors.len(); - let (module_filename, err) = - unescape_unquote_string(&import_pattern.head.name, import_pattern.head.span); - if let Some(err) = err { - working_set.error(err); - } - - if starting_error_count == working_set.parse_errors.len() { - if let Some(module_path) = - find_in_dirs(&module_filename, working_set, &cwd, LIB_DIRS_VAR) - { - if let Some(i) = working_set - .parsed_module_files - .iter() - .rposition(|p| p == &module_path) - { - let mut files: Vec = working_set - .parsed_module_files - .split_off(i) - .iter() - .map(|p| p.to_string_lossy().to_string()) - .collect(); - - files.push(module_path.to_string_lossy().to_string()); - - let msg = files.join("\nuses "); - - working_set.error(ParseError::CyclicalModuleImport( - msg, - import_pattern.head.span, - )); - return ( - Pipeline::from_vec(vec![Expression { - expr: Expr::Call(call), - span: call_span, - ty: Type::Any, - custom_completion: None, - }]), - vec![], - ); - } - - let module_name = if let Some(stem) = module_path.file_stem() { - stem.to_string_lossy().to_string() - } else { - working_set.error(ParseError::ModuleNotFound(import_pattern.head.span)); - return ( - Pipeline::from_vec(vec![Expression { - expr: Expr::Call(call), - span: call_span, - ty: Type::Any, - custom_completion: None, - }]), - vec![], - ); - }; - - if let Ok(contents) = std::fs::read(&module_path) { - let file_id = - working_set.add_file(module_path.to_string_lossy().to_string(), &contents); - let new_span = working_set.get_span_for_file(file_id); - - // Change the currently parsed directory - let prev_currently_parsed_cwd = if let Some(parent) = module_path.parent() { - let prev = working_set.currently_parsed_cwd.clone(); - - working_set.currently_parsed_cwd = Some(parent.into()); - - prev - } else { - working_set.currently_parsed_cwd.clone() - }; - - // Add the file to the stack of parsed module files - working_set.parsed_module_files.push(module_path); - - // Parse the module - let (block, module, module_comments) = - parse_module_block(working_set, new_span, module_name.as_bytes()); - - // Remove the file from the stack of parsed module files - working_set.parsed_module_files.pop(); - - // Restore the currently parsed directory back - working_set.currently_parsed_cwd = prev_currently_parsed_cwd; - - let _ = working_set.add_block(block); - let module_id = - working_set.add_module(&module_name, module.clone(), module_comments); - - ( - ImportPattern { - head: ImportPatternHead { - name: module_name.into(), - id: Some(module_id), - span: import_pattern.head.span, - }, - members: import_pattern.members, - hidden: HashSet::new(), - }, - module, - ) - } else { - working_set.error(ParseError::ModuleNotFound(import_pattern.head.span)); - return ( - Pipeline::from_vec(vec![Expression { - expr: Expr::Call(call), - span: call_span, - ty: Type::Any, - custom_completion: None, - }]), - vec![], - ); - } - } else { - working_set.error(ParseError::ModuleNotFound(import_pattern.head.span)); - return ( - Pipeline::from_vec(vec![Expression { - expr: Expr::Call(call), - span: span(spans), - ty: Type::Any, - custom_completion: None, - }]), - vec![], - ); - } - } else { - working_set.error(ParseError::NonUtf8(import_pattern.head.span)); - return (garbage_pipeline(spans), vec![]); - } + working_set.error(ParseError::ModuleNotFound(import_pattern.head.span)); + return ( + Pipeline::from_vec(vec![Expression { + expr: Expr::Call(call), + span: call_span, + ty: Type::Any, + custom_completion: None, + }]), + vec![], + ); }; - let decls_to_use = if import_pattern.members.is_empty() { - module.decls_with_head(&import_pattern.head.name) - } else { - match &import_pattern.members[0] { - ImportPatternMember::Glob { .. } => module.decls(), - ImportPatternMember::Name { name, span } => { - let mut decl_output = vec![]; + let (definitions, errors) = + module.resolve_import_pattern(working_set, module_id, &import_pattern.members, None); + working_set.parse_errors.extend(errors); - if name == b"main" { - if let Some(id) = &module.main { - decl_output.push((import_pattern.head.name.clone(), *id)); - } else { - working_set.error(ParseError::ExportNotFound(*span)); - } - } else if let Some(id) = module.get_decl_id(name) { - decl_output.push((name.clone(), id)); - } else { - working_set.error(ParseError::ExportNotFound(*span)); - } - - decl_output - } - ImportPatternMember::List { names } => { - let mut decl_output = vec![]; - - for (name, span) in names { - if name == b"main" { - if let Some(id) = &module.main { - decl_output.push((import_pattern.head.name.clone(), *id)); - } else { - working_set.error(ParseError::ExportNotFound(*span)); - } - } else if let Some(id) = module.get_decl_id(name) { - decl_output.push((name.clone(), id)); - } else { - working_set.error(ParseError::ExportNotFound(*span)); - break; - } - } - - decl_output - } - } - }; - - let exportables = decls_to_use + let exportables = definitions + .decls .iter() .map(|(name, decl_id)| Exportable::Decl { name: name.clone(), id: *decl_id, }) + .chain( + definitions + .modules + .iter() + .map(|(name, module_id)| Exportable::Module { + name: name.clone(), + id: *module_id, + }), + ) .collect(); // Extend the current scope with the module's exportables - working_set.use_decls(decls_to_use); + working_set.use_decls(definitions.decls); + working_set.use_modules(definitions.modules); - // Create a new Use command call to pass the new import pattern + // Create a new Use command call to pass the import pattern as parser info let import_pattern_expr = Expression { expr: Expr::ImportPattern(import_pattern), span: span(args_spans), @@ -2112,7 +2426,13 @@ pub fn parse_overlay_new(working_set: &mut StateWorkingSet, call: Box) -> vec![], ); - working_set.add_overlay(overlay_name.as_bytes().to_vec(), module_id, vec![], false); + working_set.add_overlay( + overlay_name.as_bytes().to_vec(), + module_id, + vec![], + vec![], + false, + ); pipeline } @@ -2181,8 +2501,6 @@ pub fn parse_overlay_use(working_set: &mut StateWorkingSet, call: Box) -> custom_completion: None, }]); - let cwd = working_set.get_cwd(); - let (final_overlay_name, origin_module, origin_module_id, is_module_updated) = if let Some(overlay_frame) = working_set.find_overlay(overlay_name.as_bytes()) { // Activate existing overlay @@ -2243,7 +2561,7 @@ pub fn parse_overlay_use(working_set: &mut StateWorkingSet, call: Box) -> (overlay_name, Module::new(module_name), module_id, true) } } else { - // Create a new overlay from a module + // Create a new overlay if let Some(module_id) = // the name is a module working_set.find_module(overlay_name.as_bytes()) @@ -2254,89 +2572,61 @@ pub fn parse_overlay_use(working_set: &mut StateWorkingSet, call: Box) -> module_id, true, ) + } else if let Some(module_id) = parse_module_file_or_dir( + working_set, + overlay_name.as_bytes(), + overlay_name_span, + new_name.as_ref().map(|spanned| spanned.item.clone()), + ) { + // try file or directory + let new_module = working_set.get_module(module_id).clone(); + ( + new_name + .map(|spanned| spanned.item) + .unwrap_or(String::from_utf8_lossy(&new_module.name).to_string()), + new_module, + module_id, + true, + ) } else { - // try if the name is a file - if let Ok(module_filename) = - String::from_utf8(trim_quotes(overlay_name.as_bytes()).to_vec()) - { - if let Some(module_path) = - find_in_dirs(&module_filename, working_set, &cwd, LIB_DIRS_VAR) - { - let overlay_name = if let Some(stem) = module_path.file_stem() { - stem.to_string_lossy().to_string() - } else { - working_set - .error(ParseError::ModuleOrOverlayNotFound(overlay_name_span)); - return pipeline; - }; - - if let Ok(contents) = std::fs::read(&module_path) { - let file_id = working_set.add_file(module_filename, &contents); - let new_span = working_set.get_span_for_file(file_id); - - // Change currently parsed directory - let prev_currently_parsed_cwd = - if let Some(parent) = module_path.parent() { - let prev = working_set.currently_parsed_cwd.clone(); - - working_set.currently_parsed_cwd = Some(parent.into()); - - prev - } else { - working_set.currently_parsed_cwd.clone() - }; - - let (block, module, module_comments) = - parse_module_block(working_set, new_span, overlay_name.as_bytes()); - - // Restore the currently parsed directory back - working_set.currently_parsed_cwd = prev_currently_parsed_cwd; - - let _ = working_set.add_block(block); - let module_id = working_set.add_module( - &overlay_name, - module.clone(), - module_comments, - ); - - ( - new_name.map(|spanned| spanned.item).unwrap_or(overlay_name), - module, - module_id, - true, - ) - } else { - working_set - .error(ParseError::ModuleOrOverlayNotFound(overlay_name_span)); - return pipeline; - } - } else { - working_set.error(ParseError::ModuleOrOverlayNotFound(overlay_name_span)); - return pipeline; - } - } else { - working_set.error(ParseError::NonUtf8(overlay_name_span)); - return garbage_pipeline(&[call_span]); - } + working_set.error(ParseError::ModuleOrOverlayNotFound(overlay_name_span)); + return pipeline; } }; - let decls_to_lay = if is_module_updated { + let (definitions, errors) = if is_module_updated { if has_prefix { - origin_module.decls_with_head(final_overlay_name.as_bytes()) + origin_module.resolve_import_pattern( + working_set, + origin_module_id, + &[], + Some(final_overlay_name.as_bytes()), + ) } else { - origin_module.decls() + origin_module.resolve_import_pattern( + working_set, + origin_module_id, + &[ImportPatternMember::Glob { + span: overlay_name_span, + }], + Some(final_overlay_name.as_bytes()), + ) } } else { - vec![] + (ResolvedImportPattern::new(vec![], vec![]), vec![]) }; - working_set.add_overlay( - final_overlay_name.as_bytes().to_vec(), - origin_module_id, - decls_to_lay, - has_prefix, - ); + if errors.is_empty() { + working_set.add_overlay( + final_overlay_name.as_bytes().to_vec(), + origin_module_id, + definitions.decls, + definitions.modules, + has_prefix, + ); + } else { + working_set.parse_errors.extend(errors); + } // Change the call argument to include the Overlay expression with the module ID let mut call = call; diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index 031c11fde9..484757b0a8 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -3009,7 +3009,10 @@ pub fn parse_type(_working_set: &StateWorkingSet, bytes: &[u8]) -> Type { pub fn parse_import_pattern(working_set: &mut StateWorkingSet, spans: &[Span]) -> Expression { let Some(head_span) = spans.get(0) else { - working_set.error(ParseError::WrongImportPattern(span(spans))); + working_set.error(ParseError::WrongImportPattern( + "needs at least one component of import pattern".to_string(), + span(spans), + )); return garbage(span(spans)); }; @@ -3029,98 +3032,87 @@ pub fn parse_import_pattern(working_set: &mut StateWorkingSet, spans: &[Span]) - } }; - let (import_pattern, err) = if let Some(tail_span) = spans.get(1) { - // FIXME: expand this to handle deeper imports once we support module imports - let tail = working_set.get_span_contents(*tail_span); - if tail == b"*" { - ( - ImportPattern { - head: ImportPatternHead { - name: head_name, - id: maybe_module_id, - span: *head_span, - }, - members: vec![ImportPatternMember::Glob { span: *tail_span }], - hidden: HashSet::new(), - }, - None, - ) - } else if tail.starts_with(b"[") { - let result = parse_list_expression(working_set, *tail_span, &SyntaxShape::String); + let mut import_pattern = ImportPattern { + head: ImportPatternHead { + name: head_name, + id: maybe_module_id, + span: *head_span, + }, + members: vec![], + hidden: HashSet::new(), + }; - let mut output = vec![]; + if spans.len() > 1 { + let mut leaf_member_span = None; - match result { - Expression { + for tail_span in spans[1..].iter() { + if let Some(prev_span) = leaf_member_span { + let what = if working_set.get_span_contents(prev_span) == b"*" { + "glob" + } else { + "list" + }; + working_set.error(ParseError::WrongImportPattern( + format!( + "{} member can be only at the end of an import pattern", + what + ), + prev_span, + )); + return Expression { + expr: Expr::ImportPattern(import_pattern), + span: prev_span, + ty: Type::List(Box::new(Type::String)), + custom_completion: None, + }; + } + + let tail = working_set.get_span_contents(*tail_span); + + if tail == b"*" { + import_pattern + .members + .push(ImportPatternMember::Glob { span: *tail_span }); + + leaf_member_span = Some(*tail_span); + } else if tail.starts_with(b"[") { + let result = parse_list_expression(working_set, *tail_span, &SyntaxShape::String); + + let mut output = vec![]; + + if let Expression { expr: Expr::List(list), .. - } => { + } = result + { for expr in list { let contents = working_set.get_span_contents(expr.span); output.push((trim_quotes(contents).to_vec(), expr.span)); } - ( - ImportPattern { - head: ImportPatternHead { - name: head_name, - id: maybe_module_id, - span: *head_span, - }, - members: vec![ImportPatternMember::List { names: output }], - hidden: HashSet::new(), - }, - None, - ) + import_pattern + .members + .push(ImportPatternMember::List { names: output }); + } else { + working_set.error(ParseError::ExportNotFound(result.span)); + return Expression { + expr: Expr::ImportPattern(import_pattern), + span: span(spans), + ty: Type::List(Box::new(Type::String)), + custom_completion: None, + }; } - _ => ( - ImportPattern { - head: ImportPatternHead { - name: head_name, - id: maybe_module_id, - span: *head_span, - }, - members: vec![], - hidden: HashSet::new(), - }, - Some(ParseError::ExportNotFound(result.span)), - ), - } - } else { - let tail = trim_quotes(tail); - ( - ImportPattern { - head: ImportPatternHead { - name: head_name, - id: maybe_module_id, - span: *head_span, - }, - members: vec![ImportPatternMember::Name { - name: tail.to_vec(), - span: *tail_span, - }], - hidden: HashSet::new(), - }, - None, - ) - } - } else { - ( - ImportPattern { - head: ImportPatternHead { - name: head_name, - id: maybe_module_id, - span: *head_span, - }, - members: vec![], - hidden: HashSet::new(), - }, - None, - ) - }; - if let Some(err) = err { - working_set.error(err); + leaf_member_span = Some(*tail_span); + } else { + let tail = trim_quotes(tail); + + import_pattern.members.push(ImportPatternMember::Name { + name: tail.to_vec(), + span: *tail_span, + }); + } + } } Expression { @@ -5206,7 +5198,7 @@ pub fn parse_builtin_commands( Pipeline::from_vec(vec![expr]) } b"alias" => parse_alias(working_set, lite_command, None), - b"module" => parse_module(working_set, lite_command), + b"module" => parse_module(working_set, lite_command, None), b"use" => { let (pipeline, _) = parse_use(working_set, &lite_command.parts); pipeline diff --git a/crates/nu-protocol/src/engine/engine_state.rs b/crates/nu-protocol/src/engine/engine_state.rs index e2cd1eba59..1c81c1ec2a 100644 --- a/crates/nu-protocol/src/engine/engine_state.rs +++ b/crates/nu-protocol/src/engine/engine_state.rs @@ -1202,6 +1202,15 @@ impl<'a> StateWorkingSet<'a> { } } + pub fn use_modules(&mut self, modules: Vec<(Vec, ModuleId)>) { + let overlay_frame = self.last_overlay_mut(); + + for (name, module_id) in modules { + overlay_frame.insert_module(name, module_id); + // overlay_frame.visibility.use_module_id(&module_id); // TODO: Add hiding modules + } + } + pub fn add_predecl(&mut self, decl: Box) -> Option { let name = decl.name().as_bytes().to_vec(); @@ -1770,6 +1779,18 @@ impl<'a> StateWorkingSet<'a> { } } + pub fn get_module_mut(&mut self, module_id: ModuleId) -> &mut Module { + let num_permanent_modules = self.permanent_state.num_modules(); + if module_id < num_permanent_modules { + panic!("Attempt to mutate a module that is in the permanent (immutable) state") + } else { + self.delta + .modules + .get_mut(module_id - num_permanent_modules) + .expect("internal error: missing module") + } + } + pub fn get_block_mut(&mut self, block_id: BlockId) -> &mut Block { let num_permanent_blocks = self.permanent_state.num_blocks(); if block_id < num_permanent_blocks { @@ -1848,7 +1869,7 @@ impl<'a> StateWorkingSet<'a> { let name = self.last_overlay_name().to_vec(); let origin = overlay_frame.origin; let prefixed = overlay_frame.prefixed; - self.add_overlay(name, origin, vec![], prefixed); + self.add_overlay(name, origin, vec![], vec![], prefixed); } self.delta @@ -1886,6 +1907,7 @@ impl<'a> StateWorkingSet<'a> { name: Vec, origin: ModuleId, decls: Vec<(Vec, DeclId)>, + modules: Vec<(Vec, ModuleId)>, prefixed: bool, ) { let last_scope_frame = self.delta.last_scope_frame_mut(); @@ -1913,6 +1935,7 @@ impl<'a> StateWorkingSet<'a> { self.move_predecls_to_overlay(); self.use_decls(decls); + self.use_modules(modules); } pub fn remove_overlay(&mut self, name: &[u8], keep_custom: bool) { diff --git a/crates/nu-protocol/src/engine/overlay.rs b/crates/nu-protocol/src/engine/overlay.rs index 6cd5cb6e6d..640763bd37 100644 --- a/crates/nu-protocol/src/engine/overlay.rs +++ b/crates/nu-protocol/src/engine/overlay.rs @@ -206,6 +206,10 @@ impl OverlayFrame { self.decls.insert((name, input), decl_id) } + pub fn insert_module(&mut self, name: Vec, module_id: ModuleId) -> Option { + self.modules.insert(name, module_id) + } + pub fn get_decl(&self, name: &[u8], input: &Type) -> Option { if let Some(decl) = self.decls.get(&(name, input) as &dyn DeclKey) { Some(*decl) diff --git a/crates/nu-protocol/src/exportable.rs b/crates/nu-protocol/src/exportable.rs index 0ce411b13b..8be71a645c 100644 --- a/crates/nu-protocol/src/exportable.rs +++ b/crates/nu-protocol/src/exportable.rs @@ -1,5 +1,6 @@ -use crate::DeclId; +use crate::{DeclId, ModuleId}; pub enum Exportable { Decl { name: Vec, id: DeclId }, + Module { name: Vec, id: ModuleId }, } diff --git a/crates/nu-protocol/src/module.rs b/crates/nu-protocol/src/module.rs index c925e95fe5..c46174ace0 100644 --- a/crates/nu-protocol/src/module.rs +++ b/crates/nu-protocol/src/module.rs @@ -1,12 +1,26 @@ -use crate::{BlockId, DeclId, Span}; +use crate::{ + ast::ImportPatternMember, engine::StateWorkingSet, BlockId, DeclId, ModuleId, ParseError, Span, +}; use indexmap::IndexMap; +pub struct ResolvedImportPattern { + pub decls: Vec<(Vec, DeclId)>, + pub modules: Vec<(Vec, ModuleId)>, +} + +impl ResolvedImportPattern { + pub fn new(decls: Vec<(Vec, DeclId)>, modules: Vec<(Vec, ModuleId)>) -> Self { + ResolvedImportPattern { decls, modules } + } +} + /// Collection of definitions that can be exported from a module #[derive(Debug, Clone)] pub struct Module { pub name: Vec, pub decls: IndexMap, DeclId>, + pub submodules: IndexMap, ModuleId>, pub env_block: Option, // `export-env { ... }` block pub main: Option, // `export def main` pub span: Option, @@ -17,6 +31,7 @@ impl Module { Module { name, decls: IndexMap::new(), + submodules: IndexMap::new(), env_block: None, main: None, span: None, @@ -27,32 +42,29 @@ impl Module { Module { name, decls: IndexMap::new(), + submodules: IndexMap::new(), env_block: None, main: None, span: Some(span), } } + pub fn name(&self) -> Vec { + self.name.clone() + } + pub fn add_decl(&mut self, name: Vec, decl_id: DeclId) -> Option { self.decls.insert(name, decl_id) } + pub fn add_submodule(&mut self, name: Vec, module_id: ModuleId) -> Option { + self.submodules.insert(name, module_id) + } + pub fn add_env_block(&mut self, block_id: BlockId) { self.env_block = Some(block_id); } - pub fn extend(&mut self, other: &Module) { - self.decls.extend(other.decls.clone()); - } - - pub fn is_empty(&self) -> bool { - self.decls.is_empty() - } - - pub fn get_decl_id(&self, name: &[u8]) -> Option { - self.decls.get(name).copied() - } - pub fn has_decl(&self, name: &[u8]) -> bool { if name == self.name && self.main.is_some() { return true; @@ -61,6 +73,120 @@ impl Module { self.decls.contains_key(name) } + pub fn resolve_import_pattern( + &self, + working_set: &StateWorkingSet, + self_id: ModuleId, + members: &[ImportPatternMember], + name_override: Option<&[u8]>, // name under the module was stored (doesn't have to be the + // same as self.name) + ) -> (ResolvedImportPattern, Vec) { + let final_name = name_override.unwrap_or(&self.name).to_vec(); + + let (head, rest) = if let Some((head, rest)) = members.split_first() { + (head, rest) + } else { + // Import pattern was just name without any members + let mut results = vec![]; + let mut errors = vec![]; + + for (_, id) in &self.submodules { + let submodule = working_set.get_module(*id); + let (sub_results, sub_errors) = + submodule.resolve_import_pattern(working_set, *id, &[], None); + errors.extend(sub_errors); + + for (sub_name, sub_decl_id) in sub_results.decls { + let mut new_name = final_name.clone(); + new_name.push(b' '); + new_name.extend(sub_name); + + results.push((new_name, sub_decl_id)); + } + } + + results.extend(self.decls_with_head(&final_name)); + + return ( + ResolvedImportPattern::new(results, vec![(final_name, self_id)]), + errors, + ); + }; + + match head { + ImportPatternMember::Name { name, span } => { + if name == b"main" { + if let Some(main_decl_id) = self.main { + ( + ResolvedImportPattern::new(vec![(final_name, main_decl_id)], vec![]), + vec![], + ) + } else { + ( + ResolvedImportPattern::new(vec![], vec![]), + vec![ParseError::ExportNotFound(*span)], + ) + } + } else if let Some(decl_id) = self.decls.get(name) { + ( + ResolvedImportPattern::new(vec![(name.clone(), *decl_id)], vec![]), + vec![], + ) + } else if let Some(submodule_id) = self.submodules.get(name) { + let submodule = working_set.get_module(*submodule_id); + submodule.resolve_import_pattern(working_set, *submodule_id, rest, None) + } else { + ( + ResolvedImportPattern::new(vec![], vec![]), + vec![ParseError::ExportNotFound(*span)], + ) + } + } + ImportPatternMember::Glob { .. } => { + let mut decls = vec![]; + let mut submodules = vec![]; + let mut errors = vec![]; + + for (_, id) in &self.submodules { + let submodule = working_set.get_module(*id); + let (sub_results, sub_errors) = + submodule.resolve_import_pattern(working_set, *id, &[], None); + decls.extend(sub_results.decls); + submodules.extend(sub_results.modules); + errors.extend(sub_errors); + } + + decls.extend(self.decls()); + submodules.extend(self.submodules()); + + (ResolvedImportPattern::new(decls, submodules), errors) + } + ImportPatternMember::List { names } => { + let mut decls = vec![]; + let mut submodules = vec![]; + let mut errors = vec![]; + + for (name, span) in names { + if name == b"main" { + if let Some(main_decl_id) = self.main { + decls.push((final_name.clone(), main_decl_id)); + } else { + errors.push(ParseError::ExportNotFound(*span)); + } + } else if let Some(decl_id) = self.decls.get(name) { + decls.push((name.clone(), *decl_id)); + } else if let Some(submodule_id) = self.submodules.get(name) { + submodules.push((name.clone(), *submodule_id)); + } else { + errors.push(ParseError::ExportNotFound(*span)); + } + } + + (ResolvedImportPattern::new(decls, submodules), errors) + } + } + } + pub fn decl_name_with_head(&self, name: &[u8], head: &[u8]) -> Option> { if self.has_decl(name) { let mut new_name = head.to_vec(); @@ -124,6 +250,13 @@ impl Module { result } + pub fn submodules(&self) -> Vec<(Vec, ModuleId)> { + self.submodules + .iter() + .map(|(name, id)| (name.clone(), *id)) + .collect() + } + pub fn decl_names(&self) -> Vec> { let mut result: Vec> = self.decls.keys().cloned().collect(); diff --git a/crates/nu-protocol/src/parse_error.rs b/crates/nu-protocol/src/parse_error.rs index c2d842d363..5c6278ed2e 100644 --- a/crates/nu-protocol/src/parse_error.rs +++ b/crates/nu-protocol/src/parse_error.rs @@ -200,14 +200,36 @@ pub enum ParseError { #[error("Can't export {0} named same as the module.")] #[diagnostic( code(nu::parser::named_as_module), - help("Module {1} can't export {0} named the same as the module. Either change the module name, or export `main` custom command.") + help("Module {1} can't export {0} named the same as the module. Either change the module name, or export `{2}` {0}.") )] NamedAsModule( + String, String, String, #[label = "can't export from module {1}"] Span, ), + #[error("Module already contains 'main' command.")] + #[diagnostic( + code(nu::parser::module_double_main), + help("Tried to add 'main' command to module '{0}' but it has already been added.") + )] + ModuleDoubleMain( + String, + #[label = "module '{0}' already contains 'main'"] Span, + ), + + #[error("Invalid module file name")] + #[diagnostic( + code(nu::parser::invalid_module_file_name), + help("File {0} resolves to module name {1} which is the same as the parent module. Either rename the file or, save it as 'mod.nu' to define the parent module.") + )] + InvalidModuleFileName( + String, + String, + #[label = "submodule can't have the same name as the parent module"] Span, + ), + #[error("Can't export alias defined as 'main'.")] #[diagnostic( code(nu::parser::export_main_alias_not_allowed), @@ -371,7 +393,7 @@ pub enum ParseError { #[error("Wrong import pattern structure.")] #[diagnostic(code(nu::parser::missing_import_pattern))] - WrongImportPattern(#[label = "invalid import pattern structure"] Span), + WrongImportPattern(String, #[label = "{0}"] Span), #[error("Export not found.")] #[diagnostic(code(nu::parser::export_not_found))] @@ -452,7 +474,9 @@ impl ParseError { ParseError::AliasNotValid(s) => *s, ParseError::CommandDefNotValid(s) => *s, ParseError::ModuleNotFound(s) => *s, - ParseError::NamedAsModule(_, _, s) => *s, + ParseError::NamedAsModule(_, _, _, s) => *s, + ParseError::ModuleDoubleMain(_, s) => *s, + ParseError::InvalidModuleFileName(_, _, s) => *s, ParseError::ExportMainAliasNotAllowed(s) => *s, ParseError::CyclicalModuleImport(_, s) => *s, ParseError::ModuleOrOverlayNotFound(s) => *s, @@ -486,7 +510,7 @@ impl ParseError { ParseError::MissingColumns(_, s) => *s, ParseError::AssignmentMismatch(_, _, s) => *s, ParseError::MissingImportPattern(s) => *s, - ParseError::WrongImportPattern(s) => *s, + ParseError::WrongImportPattern(_, s) => *s, ParseError::ExportNotFound(s) => *s, ParseError::SourcedFileNotFound(_, s) => *s, ParseError::RegisteredFileNotFound(_, s) => *s, diff --git a/tests/modules/mod.rs b/tests/modules/mod.rs index ed2db7edad..fb53bfba93 100644 --- a/tests/modules/mod.rs +++ b/tests/modules/mod.rs @@ -563,3 +563,154 @@ fn main_inside_module_is_main() { assert_eq!(actual.out, "foo"); } + +#[test] +fn module_as_file() { + let inp = &[r#"module samples/spam.nu"#, "use spam foo", "foo"]; + + let actual = nu!(cwd: "tests/modules", pipeline(&inp.join("; "))); + + assert_eq!(actual.out, "foo"); +} + +#[test] +fn export_module_as_file() { + let inp = &[r#"export module samples/spam.nu"#, "use spam foo", "foo"]; + + let actual = nu!(cwd: "tests/modules", pipeline(&inp.join("; "))); + + assert_eq!(actual.out, "foo"); +} + +#[test] +fn deep_import_patterns() { + let module_decl = r#" + module spam { + export module eggs { + export module beans { + export def foo [] { 'foo' }; + export def bar [] { 'bar' } + }; + }; + } + "#; + + let inp = &[module_decl, "use spam", "spam eggs beans foo"]; + let actual = nu!(cwd: ".", pipeline(&inp.join("; "))); + assert_eq!(actual.out, "foo"); + + let inp = &[module_decl, "use spam eggs", "eggs beans foo"]; + let actual = nu!(cwd: ".", pipeline(&inp.join("; "))); + assert_eq!(actual.out, "foo"); + + let inp = &[module_decl, "use spam eggs beans", "beans foo"]; + let actual = nu!(cwd: ".", pipeline(&inp.join("; "))); + assert_eq!(actual.out, "foo"); + + let inp = &[module_decl, "use spam eggs beans foo", "foo"]; + let actual = nu!(cwd: ".", pipeline(&inp.join("; "))); + assert_eq!(actual.out, "foo"); +} + +#[test] +fn module_dir() { + let import = "use samples/spam"; + + let inp = &[import, "spam"]; + let actual = nu!(cwd: "tests/modules", pipeline(&inp.join("; "))); + assert_eq!(actual.out, "spam"); + + let inp = &[import, "spam foo"]; + let actual = nu!(cwd: "tests/modules", pipeline(&inp.join("; "))); + assert_eq!(actual.out, "foo"); + + let inp = &[import, "spam bar"]; + let actual = nu!(cwd: "tests/modules", pipeline(&inp.join("; "))); + assert_eq!(actual.out, "bar"); + + let inp = &[import, "spam foo baz"]; + let actual = nu!(cwd: "tests/modules", pipeline(&inp.join("; "))); + assert_eq!(actual.out, "foobaz"); + + let inp = &[import, "spam bar baz"]; + let actual = nu!(cwd: "tests/modules", pipeline(&inp.join("; "))); + assert_eq!(actual.out, "barbaz"); + + let inp = &[import, "spam baz"]; + let actual = nu!(cwd: "tests/modules", pipeline(&inp.join("; "))); + assert_eq!(actual.out, "spambaz"); +} + +#[test] +fn not_allowed_submodule_file() { + let inp = &["use samples/not_allowed"]; + let actual = nu!(cwd: "tests/modules", pipeline(&inp.join("; "))); + assert!(actual.err.contains("invalid_module_file_name")); +} + +#[test] +fn allowed_local_module() { + let inp = &["module spam { module spam {} }"]; + let actual = nu!(cwd: "tests/modules", pipeline(&inp.join("; "))); + assert!(actual.err.is_empty()); +} + +#[test] +fn not_allowed_submodule() { + let inp = &["module spam { export module spam {} }"]; + let actual = nu!(cwd: "tests/modules", pipeline(&inp.join("; "))); + assert!(actual.err.contains("named_as_module")); +} + +#[test] +fn module_self_name() { + let inp = &[ + "module spam { export module mod { export def main [] { 'spam' } } }", + "use spam", + "spam", + ]; + let actual = nu!(cwd: "tests/modules", pipeline(&inp.join("; "))); + assert_eq!(actual.out, "spam"); +} + +#[test] +fn module_self_name_main_not_allowed() { + let inp = &[ + r#"module spam { + export def main [] { 'main spam' }; + + export module mod { + export def main [] { 'mod spam' } + } + }"#, + "use spam", + "spam", + ]; + let actual = nu!(cwd: "tests/modules", pipeline(&inp.join("; "))); + assert!(actual.err.contains("module_double_main")); + + let inp = &[ + r#"module spam { + export module mod { + export def main [] { 'mod spam' } + }; + + export def main [] { 'main spam' } + }"#, + "use spam", + "spam", + ]; + let actual = nu!(cwd: "tests/modules", pipeline(&inp.join("; "))); + assert!(actual.err.contains("module_double_main")); +} + +#[test] +fn module_main_not_found() { + let inp = &["module spam {}", "use spam main"]; + let actual = nu!(cwd: "tests/modules", pipeline(&inp.join("; "))); + assert!(actual.err.contains("export_not_found")); + + let inp = &["module spam {}", "use spam [ main ]"]; + let actual = nu!(cwd: "tests/modules", pipeline(&inp.join("; "))); + assert!(actual.err.contains("export_not_found")); +} diff --git a/tests/modules/samples/not_allowed/not_allowed.nu b/tests/modules/samples/not_allowed/not_allowed.nu new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/modules/samples/spam.nu b/tests/modules/samples/spam.nu new file mode 100644 index 0000000000..605a4510d9 --- /dev/null +++ b/tests/modules/samples/spam.nu @@ -0,0 +1 @@ +export def foo [] { 'foo' } diff --git a/tests/modules/samples/spam/bar.nu b/tests/modules/samples/spam/bar.nu new file mode 100644 index 0000000000..35c4e75afe --- /dev/null +++ b/tests/modules/samples/spam/bar.nu @@ -0,0 +1,3 @@ +export def main [] { 'bar' } + +export def baz [] { 'barbaz' } diff --git a/tests/modules/samples/spam/foo.nu b/tests/modules/samples/spam/foo.nu new file mode 100644 index 0000000000..5541e1e826 --- /dev/null +++ b/tests/modules/samples/spam/foo.nu @@ -0,0 +1,3 @@ +export def main [] { 'foo' } + +export def baz [] { 'foobaz' } diff --git a/tests/modules/samples/spam/mod.nu b/tests/modules/samples/spam/mod.nu new file mode 100644 index 0000000000..f6c53f6081 --- /dev/null +++ b/tests/modules/samples/spam/mod.nu @@ -0,0 +1,3 @@ +export def main [] { 'spam' } + +export def baz [] { 'spambaz' } diff --git a/tests/overlays/mod.rs b/tests/overlays/mod.rs index 0579f9c3fc..25c35f82cd 100644 --- a/tests/overlays/mod.rs +++ b/tests/overlays/mod.rs @@ -1313,6 +1313,64 @@ fn alias_overlay_new() { assert_eq!(actual_repl.out, "eggs"); } +#[test] +fn overlay_use_module_dir() { + let import = "overlay use samples/spam"; + + let inp = &[import, "spam"]; + let actual = nu!(cwd: "tests/modules", pipeline(&inp.join("; "))); + assert_eq!(actual.out, "spam"); + + let inp = &[import, "foo"]; + let actual = nu!(cwd: "tests/modules", pipeline(&inp.join("; "))); + assert_eq!(actual.out, "foo"); + + let inp = &[import, "bar"]; + let actual = nu!(cwd: "tests/modules", pipeline(&inp.join("; "))); + assert_eq!(actual.out, "bar"); + + let inp = &[import, "foo baz"]; + let actual = nu!(cwd: "tests/modules", pipeline(&inp.join("; "))); + assert_eq!(actual.out, "foobaz"); + + let inp = &[import, "bar baz"]; + let actual = nu!(cwd: "tests/modules", pipeline(&inp.join("; "))); + assert_eq!(actual.out, "barbaz"); + + let inp = &[import, "baz"]; + let actual = nu!(cwd: "tests/modules", pipeline(&inp.join("; "))); + assert_eq!(actual.out, "spambaz"); +} + +#[test] +fn overlay_use_module_dir_prefix() { + let import = "overlay use samples/spam --prefix"; + + let inp = &[import, "spam"]; + let actual = nu!(cwd: "tests/modules", pipeline(&inp.join("; "))); + assert_eq!(actual.out, "spam"); + + let inp = &[import, "spam foo"]; + let actual = nu!(cwd: "tests/modules", pipeline(&inp.join("; "))); + assert_eq!(actual.out, "foo"); + + let inp = &[import, "spam bar"]; + let actual = nu!(cwd: "tests/modules", pipeline(&inp.join("; "))); + assert_eq!(actual.out, "bar"); + + let inp = &[import, "spam foo baz"]; + let actual = nu!(cwd: "tests/modules", pipeline(&inp.join("; "))); + assert_eq!(actual.out, "foobaz"); + + let inp = &[import, "spam bar baz"]; + let actual = nu!(cwd: "tests/modules", pipeline(&inp.join("; "))); + assert_eq!(actual.out, "barbaz"); + + let inp = &[import, "spam baz"]; + let actual = nu!(cwd: "tests/modules", pipeline(&inp.join("; "))); + assert_eq!(actual.out, "spambaz"); +} + #[test] fn overlay_help_no_error() { let actual = nu!(cwd: ".", "overlay hide -h");