From 7ef4e5f9401469888a37f6fdd592f65a67cd7610 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BD=C3=A1dn=C3=ADk?= Date: Tue, 23 Aug 2022 00:19:47 +0300 Subject: [PATCH] Allow parsing modules as scripts (#6357) * Allow parsing modules as scripts * Remove 'export env' from the supported keywords * Add test for export in blocks; Allow "export use" * Allow evaluating "export alias" * Fmt; Clippy * Allow running "export extern" in scripts --- crates/nu-parser/src/parse_keywords.rs | 207 +++++++++++++++++++++---- crates/nu-parser/src/parser.rs | 90 +++-------- tests/shell/mod.rs | 29 ++++ 3 files changed, 231 insertions(+), 95 deletions(-) diff --git a/crates/nu-parser/src/parse_keywords.rs b/crates/nu-parser/src/parse_keywords.rs index 846016bcc1..2412af0b6d 100644 --- a/crates/nu-parser/src/parse_keywords.rs +++ b/crates/nu-parser/src/parse_keywords.rs @@ -274,8 +274,16 @@ pub fn parse_def( // Checking that the function is used with the correct name // Maybe this is not necessary but it is a sanity check + // Note: "export def" is treated the same as "def" - let def_call = working_set.get_span_contents(spans[0]).to_vec(); + let (name_span, split_id) = + if spans.len() > 1 && working_set.get_span_contents(spans[0]) == b"export" { + (spans[1], 2) + } else { + (spans[0], 1) + }; + + let def_call = working_set.get_span_contents(name_span).to_vec(); if def_call != b"def" && def_call != b"def-env" { return ( garbage_pipeline(spans), @@ -301,14 +309,15 @@ pub fn parse_def( } Some(decl_id) => { working_set.enter_scope(); + let (command_spans, rest_spans) = spans.split_at(split_id); let ParsedInternalCall { call, error: mut err, output, } = parse_internal_call( working_set, - spans[0], - &spans[1..], + span(command_spans), + rest_spans, decl_id, expand_aliases_denylist, ); @@ -381,7 +390,7 @@ pub fn parse_def( error = error.or_else(|| { Some(ParseError::InternalError( "Predeclaration failed to add declaration".into(), - spans[1], + name_expr.span, )) }); }; @@ -415,7 +424,7 @@ pub fn parse_extern( lite_command: &LiteCommand, expand_aliases_denylist: &[usize], ) -> (Pipeline, Option) { - let spans = &lite_command.parts[..]; + let spans = &lite_command.parts; let mut error = None; let usage = build_usage(working_set, &lite_command.comments); @@ -423,7 +432,14 @@ pub fn parse_extern( // Checking that the function is used with the correct name // Maybe this is not necessary but it is a sanity check - let extern_call = working_set.get_span_contents(spans[0]).to_vec(); + let (name_span, split_id) = + if spans.len() > 1 && working_set.get_span_contents(spans[0]) == b"export" { + (spans[1], 2) + } else { + (spans[0], 1) + }; + + let extern_call = working_set.get_span_contents(name_span).to_vec(); if extern_call != b"extern" { return ( garbage_pipeline(spans), @@ -449,12 +465,15 @@ pub fn parse_extern( } Some(decl_id) => { working_set.enter_scope(); + + let (command_spans, rest_spans) = spans.split_at(split_id); + let ParsedInternalCall { call, error: err, .. } = parse_internal_call( working_set, - spans[0], - &spans[1..], + span(command_spans), + rest_spans, decl_id, expand_aliases_denylist, ); @@ -491,7 +510,7 @@ pub fn parse_extern( error = error.or_else(|| { Some(ParseError::InternalError( "Predeclaration failed to add declaration".into(), - spans[1], + spans[split_id], )) }); }; @@ -525,7 +544,14 @@ pub fn parse_alias( spans: &[Span], expand_aliases_denylist: &[usize], ) -> (Pipeline, Option) { - let name = working_set.get_span_contents(spans[0]); + let (name_span, split_id) = + if spans.len() > 1 && working_set.get_span_contents(spans[0]) == b"export" { + (spans[1], 2) + } else { + (spans[0], 1) + }; + + let name = working_set.get_span_contents(name_span); if name == b"alias" { if let Some((span, err)) = check_name(working_set, spans) { @@ -533,10 +559,12 @@ pub fn parse_alias( } if let Some(decl_id) = working_set.find_decl(b"alias", &Type::Any) { + let (command_spans, rest_spans) = spans.split_at(split_id); + let ParsedInternalCall { call, output, .. } = parse_internal_call( working_set, - spans[0], - &spans[1..], + span(command_spans), + rest_spans, decl_id, expand_aliases_denylist, ); @@ -553,8 +581,8 @@ pub fn parse_alias( ); } - if spans.len() >= 4 { - let alias_name = working_set.get_span_contents(spans[1]); + if spans.len() >= split_id + 3 { + let alias_name = working_set.get_span_contents(spans[split_id]); let alias_name = if alias_name.starts_with(b"\"") && alias_name.ends_with(b"\"") @@ -564,9 +592,9 @@ pub fn parse_alias( } else { alias_name.to_vec() }; - let _equals = working_set.get_span_contents(spans[2]); + let _equals = working_set.get_span_contents(spans[split_id + 1]); - let replacement = spans[3..].to_vec(); + let replacement = spans[(split_id + 2)..].to_vec(); working_set.add_alias(alias_name, replacement); } @@ -574,7 +602,7 @@ pub fn parse_alias( let err = if spans.len() < 4 { Some(ParseError::IncorrectValue( "Incomplete alias".into(), - spans[0], + span(&spans[..split_id]), "incomplete alias".into(), )) } else { @@ -602,7 +630,107 @@ pub fn parse_alias( ) } -pub fn parse_export( +// This one will trigger if `export` appears during eval, e.g., in a script +pub fn parse_export_in_block( + working_set: &mut StateWorkingSet, + lite_command: &LiteCommand, + expand_aliases_denylist: &[usize], +) -> (Pipeline, Option) { + let call_span = span(&lite_command.parts); + + 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"export".to_vec(), + } + } else { + b"export".to_vec() + }; + + if let Some(decl_id) = working_set.find_decl(&full_name, &Type::Any) { + let ParsedInternalCall { + call, + error: mut err, + output, + .. + } = parse_internal_call( + working_set, + if full_name == b"export" { + lite_command.parts[0] + } else { + span(&lite_command.parts[0..2]) + }, + if full_name == b"export" { + &lite_command.parts[1..] + } else { + &lite_command.parts[2..] + }, + decl_id, + expand_aliases_denylist, + ); + + let decl = working_set.get_decl(decl_id); + err = check_call(call_span, &decl.signature(), &call).or(err); + + if err.is_some() || call.has_flag("help") { + return ( + Pipeline::from_vec(vec![Expression { + expr: Expr::Call(call), + span: call_span, + ty: output, + custom_completion: None, + }]), + err, + ); + } + } else { + return ( + garbage_pipeline(&lite_command.parts), + Some(ParseError::UnknownState( + format!( + "internal error: '{}' declaration not found", + String::from_utf8_lossy(&full_name) + ), + span(&lite_command.parts), + )), + ); + }; + + if &full_name == b"export" { + // export by itself is meaningless + return ( + garbage_pipeline(&lite_command.parts), + Some(ParseError::UnexpectedKeyword( + "export".into(), + lite_command.parts[0], + )), + ); + } + + match full_name.as_slice() { + b"export alias" => parse_alias(working_set, &lite_command.parts, expand_aliases_denylist), + b"export def" | b"export def-env" => { + parse_def(working_set, lite_command, expand_aliases_denylist) + } + b"export use" => { + let (pipeline, _, err) = + parse_use(working_set, &lite_command.parts, expand_aliases_denylist); + (pipeline, err) + } + b"export extern" => parse_extern(working_set, lite_command, expand_aliases_denylist), + _ => ( + garbage_pipeline(&lite_command.parts), + Some(ParseError::UnexpectedKeyword( + String::from_utf8_lossy(&full_name).to_string(), + lite_command.parts[0], + )), + ), + } +} + +// This one will trigger only in a module +pub fn parse_export_in_module( working_set: &mut StateWorkingSet, lite_command: &LiteCommand, expand_aliases_denylist: &[usize], @@ -1163,7 +1291,7 @@ 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, exportables, err) = parse_export( + let (pipe, exportables, err) = parse_export_in_module( working_set, &pipeline.commands[0], expand_aliases_denylist, @@ -1309,7 +1437,26 @@ pub fn parse_use( spans: &[Span], expand_aliases_denylist: &[usize], ) -> (Pipeline, Vec, Option) { - if working_set.get_span_contents(spans[0]) != b"use" { + let (name_span, split_id) = + if spans.len() > 1 && working_set.get_span_contents(spans[0]) == b"export" { + (spans[1], 2) + } else { + (spans[0], 1) + }; + + let use_call = working_set.get_span_contents(name_span).to_vec(); + if use_call != b"use" { + return ( + garbage_pipeline(spans), + vec![], + Some(ParseError::UnknownState( + "internal error: Wrong call name for 'use' command".into(), + span(spans), + )), + ); + } + + if working_set.get_span_contents(name_span) != b"use" { return ( garbage_pipeline(spans), vec![], @@ -1322,14 +1469,16 @@ pub fn parse_use( let (call, call_span, use_decl_id) = match working_set.find_decl(b"use", &Type::Any) { Some(decl_id) => { + let (command_spans, rest_spans) = spans.split_at(split_id); + let ParsedInternalCall { call, error: mut err, output, } = parse_internal_call( working_set, - spans[0], - &spans[1..], + span(command_spans), + rest_spans, decl_id, expand_aliases_denylist, ); @@ -1420,7 +1569,7 @@ pub fn parse_use( custom_completion: None, }]), vec![], - Some(ParseError::ModuleNotFound(spans[1])), + Some(ParseError::ModuleNotFound(import_pattern.head.span)), ); }; @@ -1458,7 +1607,7 @@ pub fn parse_use( head: ImportPatternHead { name: module_name.into(), id: Some(module_id), - span: spans[1], + span: import_pattern.head.span, }, members: import_pattern.members, hidden: HashSet::new(), @@ -1474,14 +1623,16 @@ pub fn parse_use( custom_completion: None, }]), vec![], - Some(ParseError::ModuleNotFound(spans[1])), + Some(ParseError::ModuleNotFound(import_pattern.head.span)), ); } } else { error = error.or(Some(ParseError::ModuleNotFound(import_pattern.head.span))); + let old_span = import_pattern.head.span; + let mut import_pattern = ImportPattern::new(); - import_pattern.head.span = spans[1]; + import_pattern.head.span = old_span; (import_pattern, Module::new()) } @@ -1489,7 +1640,7 @@ pub fn parse_use( return ( garbage_pipeline(spans), vec![], - Some(ParseError::NonUtf8(spans[1])), + Some(ParseError::NonUtf8(import_pattern.head.span)), ); } }; @@ -1565,7 +1716,7 @@ pub fn parse_use( }; let call = Box::new(Call { - head: spans[0], + head: span(spans.split_at(split_id).0), decl_id: use_decl_id, arguments: vec![Argument::Positional(import_pattern_expr)], redirect_stdout: true, diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index 2b3ff37621..26840917ff 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -1,6 +1,7 @@ use crate::{ lex, lite_parse, lite_parse::LiteCommand, + parse_export_in_block, parse_keywords::{parse_extern, parse_for, parse_source}, type_check::{math_result_type, type_compatible}, LiteBlock, ParseError, Token, TokenContents, @@ -231,30 +232,42 @@ pub fn check_name<'a>( working_set: &mut StateWorkingSet, spans: &'a [Span], ) -> Option<(&'a Span, ParseError)> { + let command_len = if !spans.is_empty() { + if working_set.get_span_contents(spans[0]) == b"export" { + 2 + } else { + 1 + } + } else { + return None; + }; + if spans.len() == 1 { None - } else if spans.len() < 4 { - if working_set.get_span_contents(spans[1]) == b"=" { - let name = String::from_utf8_lossy(working_set.get_span_contents(spans[0])); + } else if spans.len() < command_len + 3 { + if working_set.get_span_contents(spans[command_len]) == b"=" { + let name = + String::from_utf8_lossy(working_set.get_span_contents(span(&spans[..command_len]))); Some(( - &spans[1], + &spans[command_len], ParseError::AssignmentMismatch( format!("{} missing name", name), "missing name".into(), - spans[1], + spans[command_len], ), )) } else { None } - } else if working_set.get_span_contents(spans[2]) != b"=" { - let name = String::from_utf8_lossy(working_set.get_span_contents(spans[0])); + } else if working_set.get_span_contents(spans[command_len + 1]) != b"=" { + let name = + String::from_utf8_lossy(working_set.get_span_contents(span(&spans[..command_len]))); Some(( - &spans[2], + &spans[command_len + 1], ParseError::AssignmentMismatch( format!("{} missing sign", name), "missing equal sign".into(), - spans[2], + spans[command_len + 1], ), )) } else { @@ -4793,64 +4806,7 @@ pub fn parse_builtin_commands( } 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" => { - let full_decl = 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"env" | b"extern" | b"use" => { - [b"export ", sub].concat() - } - _ => b"export".to_vec(), - } - } else { - b"export".to_vec() - }; - if let Some(decl_id) = working_set.find_decl(&full_decl, &Type::Any) { - let parsed_call = parse_internal_call( - working_set, - if full_decl == b"export" { - lite_command.parts[0] - } else { - span(&lite_command.parts[0..2]) - }, - if full_decl == b"export" { - &lite_command.parts[1..] - } else { - &lite_command.parts[2..] - }, - decl_id, - expand_aliases_denylist, - ); - - if parsed_call.call.has_flag("help") { - ( - Pipeline::from_vec(vec![Expression { - expr: Expr::Call(parsed_call.call), - span: span(&lite_command.parts), - ty: parsed_call.output, - custom_completion: None, - }]), - None, - ) - } else { - ( - garbage_pipeline(&lite_command.parts), - Some(ParseError::UnexpectedKeyword( - "export".into(), - lite_command.parts[0], - )), - ) - } - } else { - ( - garbage_pipeline(&lite_command.parts), - Some(ParseError::UnexpectedKeyword( - "export".into(), - lite_command.parts[0], - )), - ) - } - } + b"export" => parse_export_in_block(working_set, lite_command, expand_aliases_denylist), b"hide" => parse_hide(working_set, &lite_command.parts, expand_aliases_denylist), #[cfg(feature = "plugin")] b"register" => parse_register(working_set, &lite_command.parts, expand_aliases_denylist), diff --git a/tests/shell/mod.rs b/tests/shell/mod.rs index 5b30142119..49152c67ff 100644 --- a/tests/shell/mod.rs +++ b/tests/shell/mod.rs @@ -170,3 +170,32 @@ fn nu_lib_dirs_relative_script() { assert_eq!(actual_repl.out, "foo"); }) } + +#[test] +fn run_script_that_looks_like_module() { + Playground::setup("run_script_that_looks_like_module", |dirs, _| { + let inp_lines = &[ + r#"module spam { export def eggs [] { 'eggs' } }"#, + r#"export use spam eggs"#, + r#"export def foo [] { eggs }"#, + r#"export alias bar = foo"#, + r#"export def-env baz [] { bar }"#, + r#"baz"#, + ]; + + let actual = nu!(cwd: dirs.test(), inp_lines.join("; ")); + + assert_eq!(actual.out, "eggs"); + }) +} + +#[test] +fn run_export_extern() { + Playground::setup("run_script_that_looks_like_module", |dirs, _| { + let inp_lines = &[r#"export extern foo []"#, r#"help foo"#]; + + let actual = nu!(cwd: dirs.test(), inp_lines.join("; ")); + + assert!(actual.out.contains("Usage")); + }) +}