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
This commit is contained in:
Jakub Žádník
2022-07-29 11:57:10 +03:00
committed by GitHub
parent cf2e9cf481
commit 2cffff0c1b
10 changed files with 620 additions and 117 deletions

View File

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

View File

@ -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<nu_protocol::PipelineData, nu_protocol::ShellError> {
Ok(PipelineData::new(call.head))
}
fn examples(&self) -> Vec<Example> {
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(),
}),
}]
}
}

View File

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

View File

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

View File

@ -606,7 +606,7 @@ pub fn parse_export(
working_set: &mut StateWorkingSet,
lite_command: &LiteCommand,
expand_aliases_denylist: &[usize],
) -> (Pipeline, Option<Exportable>, Option<ParseError>) {
) -> (Pipeline, Vec<Exportable>, Option<ParseError>) {
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<ParseError>) {
) -> (Pipeline, Vec<Exportable>, Option<ParseError>) {
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 {

View File

@ -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" => {

View File

@ -1,7 +1,7 @@
use crate::{AliasId, BlockId, DeclId};
pub enum Exportable {
Decl(DeclId),
Alias(AliasId),
EnvVar(BlockId),
Decl { name: Vec<u8>, id: DeclId },
Alias { name: Vec<u8>, id: AliasId },
EnvVar { name: Vec<u8>, id: BlockId },
}

View File

@ -33,16 +33,16 @@ impl Module {
}
}
pub fn add_decl(&mut self, name: &[u8], decl_id: DeclId) -> Option<DeclId> {
self.decls.insert(name.to_vec(), decl_id)
pub fn add_decl(&mut self, name: Vec<u8>, decl_id: DeclId) -> Option<DeclId> {
self.decls.insert(name, decl_id)
}
pub fn add_alias(&mut self, name: &[u8], alias_id: AliasId) -> Option<AliasId> {
self.aliases.insert(name.to_vec(), alias_id)
pub fn add_alias(&mut self, name: Vec<u8>, alias_id: AliasId) -> Option<AliasId> {
self.aliases.insert(name, alias_id)
}
pub fn add_env_var(&mut self, name: &[u8], block_id: BlockId) -> Option<BlockId> {
self.env_vars.insert(name.to_vec(), block_id)
pub fn add_env_var(&mut self, name: Vec<u8>, block_id: BlockId) -> Option<BlockId> {
self.env_vars.insert(name, block_id)
}
pub fn extend(&mut self, other: &Module) {