Allow creating modules from directories (#9066)

This commit is contained in:
Jakub Žádník 2023-05-06 21:39:54 +03:00 committed by GitHub
parent 6dc7ff2335
commit a2a346e39c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1220 additions and 440 deletions

View File

@ -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<PipelineData, ShellError> {
Ok(PipelineData::empty())
}
fn examples(&self) -> Vec<Example> {
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 {})
}
}

View File

@ -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;

View File

@ -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)
}

View File

@ -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",

View File

@ -33,6 +33,7 @@ pub fn create_default_context() -> EngineState {
ExportDefEnv,
ExportExtern,
ExportUse,
ExportModule,
Extern,
For,
Help,

View File

@ -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<dyn Command>) -> Box<EngineState> {
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);

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -1202,6 +1202,15 @@ impl<'a> StateWorkingSet<'a> {
}
}
pub fn use_modules(&mut self, modules: Vec<(Vec<u8>, 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<dyn Command>) -> Option<DeclId> {
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<u8>,
origin: ModuleId,
decls: Vec<(Vec<u8>, DeclId)>,
modules: Vec<(Vec<u8>, 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) {

View File

@ -206,6 +206,10 @@ impl OverlayFrame {
self.decls.insert((name, input), decl_id)
}
pub fn insert_module(&mut self, name: Vec<u8>, module_id: ModuleId) -> Option<ModuleId> {
self.modules.insert(name, module_id)
}
pub fn get_decl(&self, name: &[u8], input: &Type) -> Option<DeclId> {
if let Some(decl) = self.decls.get(&(name, input) as &dyn DeclKey) {
Some(*decl)

View File

@ -1,5 +1,6 @@
use crate::DeclId;
use crate::{DeclId, ModuleId};
pub enum Exportable {
Decl { name: Vec<u8>, id: DeclId },
Module { name: Vec<u8>, id: ModuleId },
}

View File

@ -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<u8>, DeclId)>,
pub modules: Vec<(Vec<u8>, ModuleId)>,
}
impl ResolvedImportPattern {
pub fn new(decls: Vec<(Vec<u8>, DeclId)>, modules: Vec<(Vec<u8>, ModuleId)>) -> Self {
ResolvedImportPattern { decls, modules }
}
}
/// Collection of definitions that can be exported from a module
#[derive(Debug, Clone)]
pub struct Module {
pub name: Vec<u8>,
pub decls: IndexMap<Vec<u8>, DeclId>,
pub submodules: IndexMap<Vec<u8>, ModuleId>,
pub env_block: Option<BlockId>, // `export-env { ... }` block
pub main: Option<DeclId>, // `export def main`
pub span: Option<Span>,
@ -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<u8> {
self.name.clone()
}
pub fn add_decl(&mut self, name: Vec<u8>, decl_id: DeclId) -> Option<DeclId> {
self.decls.insert(name, decl_id)
}
pub fn add_submodule(&mut self, name: Vec<u8>, module_id: ModuleId) -> Option<ModuleId> {
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<DeclId> {
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<ParseError>) {
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<Vec<u8>> {
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<u8>, ModuleId)> {
self.submodules
.iter()
.map(|(name, id)| (name.clone(), *id))
.collect()
}
pub fn decl_names(&self) -> Vec<Vec<u8>> {
let mut result: Vec<Vec<u8>> = self.decls.keys().cloned().collect();

View File

@ -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,

View File

@ -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"));
}

View File

@ -0,0 +1 @@
export def foo [] { 'foo' }

View File

@ -0,0 +1,3 @@
export def main [] { 'bar' }
export def baz [] { 'barbaz' }

View File

@ -0,0 +1,3 @@
export def main [] { 'foo' }
export def baz [] { 'foobaz' }

View File

@ -0,0 +1,3 @@
export def main [] { 'spam' }
export def baz [] { 'spambaz' }

View File

@ -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");