From 88988dc9f467e6b27dbc3bc3a7b4bc388688c607 Mon Sep 17 00:00:00 2001 From: Fernando Herrera Date: Fri, 19 Nov 2021 02:51:42 +0000 Subject: [PATCH] Plugins signature load (#349) * saving signatures to file * loading plugin signature from file * is_plugin column for help command --- Cargo.lock | 2 + Cargo.toml | 2 +- crates/nu-command/src/core_commands/help.rs | 12 +++ .../nu-command/src/core_commands/register.rs | 1 + crates/nu-command/src/default_context.rs | 2 +- crates/nu-command/src/example_test.rs | 4 +- crates/nu-engine/src/eval.rs | 2 +- crates/nu-parser/Cargo.toml | 1 + crates/nu-parser/src/errors.rs | 2 +- crates/nu-parser/src/parse_keywords.rs | 26 ++++++- crates/nu-plugin/src/plugin.rs | 4 +- crates/nu-protocol/Cargo.toml | 9 ++- crates/nu-protocol/src/engine/command.rs | 4 +- crates/nu-protocol/src/engine/engine_state.rs | 75 ++++++++++++++++++- crates/nu-protocol/src/signature.rs | 11 ++- crates/nu-protocol/src/syntax_shape.rs | 4 +- crates/nu-protocol/tests/test_signature.rs | 48 ++++++++++++ src/main.rs | 33 ++++++-- 18 files changed, 215 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2dd2aee9c9..5c8e76c77b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -948,6 +948,7 @@ dependencies = [ "nu-path", "nu-plugin", "nu-protocol", + "serde_json", "thiserror", ] @@ -978,6 +979,7 @@ dependencies = [ "im", "miette", "serde", + "serde_json", "thiserror", ] diff --git a/Cargo.toml b/Cargo.toml index 55ddc25df1..8a1b32dad0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ ctrlc = "3.2.1" # mimalloc = { version = "*", default-features = false } [features] -plugin = ["nu-plugin", "nu-parser/plugin", "nu-command/plugin"] +plugin = ["nu-plugin", "nu-parser/plugin", "nu-command/plugin", "nu-protocol/plugin"] default = ["plugin"] [dev-dependencies] diff --git a/crates/nu-command/src/core_commands/help.rs b/crates/nu-command/src/core_commands/help.rs index bad246f870..3f86eb8381 100644 --- a/crates/nu-command/src/core_commands/help.rs +++ b/crates/nu-command/src/core_commands/help.rs @@ -114,6 +114,12 @@ fn help( span: head, }); + cols.push("is_plugin".into()); + vals.push(Value::Bool { + val: cmd.2, + span: head, + }); + cols.push("usage".into()); vals.push(Value::String { val: c, span: head }); @@ -157,6 +163,12 @@ fn help( span: head, }); + cols.push("is_plugin".into()); + vals.push(Value::Bool { + val: cmd.2, + span: head, + }); + cols.push("usage".into()); vals.push(Value::String { val: c, span: head }); diff --git a/crates/nu-command/src/core_commands/register.rs b/crates/nu-command/src/core_commands/register.rs index e60e2bd3ba..57ed9605a8 100644 --- a/crates/nu-command/src/core_commands/register.rs +++ b/crates/nu-command/src/core_commands/register.rs @@ -21,6 +21,7 @@ impl Command for Register { SyntaxShape::Filepath, "location of bin for plugin", ) + .optional("signature", SyntaxShape::Any, "plugin signature") .category(Category::Core) } diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 5dde288849..38f15bac65 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -146,7 +146,7 @@ pub fn create_default_context() -> EngineState { working_set.render() }; - engine_state.merge_delta(delta); + let _ = engine_state.merge_delta(delta); engine_state } diff --git a/crates/nu-command/src/example_test.rs b/crates/nu-command/src/example_test.rs index b9e6f6d535..1c7d006739 100644 --- a/crates/nu-command/src/example_test.rs +++ b/crates/nu-command/src/example_test.rs @@ -33,7 +33,7 @@ pub fn test_examples(cmd: impl Command + 'static) { working_set.render() }; - engine_state.merge_delta(delta); + let _ = engine_state.merge_delta(delta); for example in examples { // Skip tests that don't have results to compare to @@ -53,7 +53,7 @@ pub fn test_examples(cmd: impl Command + 'static) { (output, working_set.render()) }; - engine_state.merge_delta(delta); + let _ = engine_state.merge_delta(delta); let mut stack = Stack::new(); diff --git a/crates/nu-engine/src/eval.rs b/crates/nu-engine/src/eval.rs index 8e74d50fbf..ae1c4e9c7c 100644 --- a/crates/nu-engine/src/eval.rs +++ b/crates/nu-engine/src/eval.rs @@ -585,7 +585,7 @@ pub fn eval_variable( cols.push("is_plugin".to_string()); vals.push(Value::Bool { - val: decl.is_plugin(), + val: decl.is_plugin().is_some(), span, }); diff --git a/crates/nu-parser/Cargo.toml b/crates/nu-parser/Cargo.toml index 9e35e7c712..6832482b49 100644 --- a/crates/nu-parser/Cargo.toml +++ b/crates/nu-parser/Cargo.toml @@ -8,6 +8,7 @@ miette = "3.0.0" thiserror = "1.0.29" nu-protocol = { path = "../nu-protocol"} nu-plugin = { path = "../nu-plugin", optional=true} +serde_json = "1.0" nu-path = {path = "../nu-path"} [features] diff --git a/crates/nu-parser/src/errors.rs b/crates/nu-parser/src/errors.rs index eac128684f..e56248ea66 100644 --- a/crates/nu-parser/src/errors.rs +++ b/crates/nu-parser/src/errors.rs @@ -192,6 +192,6 @@ pub enum ParseError { FileNotFound(String), #[error("Plugin error")] - #[diagnostic(code(nu::parser::export_not_found), url(docsrs))] + #[diagnostic(code(nu::parser::plugin_error), url(docsrs))] PluginError(String), } diff --git a/crates/nu-parser/src/parse_keywords.rs b/crates/nu-parser/src/parse_keywords.rs index 927c4be5bb..25109d4d9e 100644 --- a/crates/nu-parser/src/parse_keywords.rs +++ b/crates/nu-parser/src/parse_keywords.rs @@ -1085,6 +1085,8 @@ pub fn parse_plugin( working_set: &mut StateWorkingSet, spans: &[Span], ) -> (Statement, Option) { + use nu_protocol::Signature; + let name = working_set.get_span_contents(spans[0]); if name != b"register" { @@ -1122,7 +1124,8 @@ pub fn parse_plugin( // store declaration in working set let plugin_decl = PluginDeclaration::new(filename.clone(), signature); - working_set.add_decl(Box::new(plugin_decl)); + + working_set.add_plugin_decl(Box::new(plugin_decl)); } None @@ -1135,8 +1138,27 @@ pub fn parse_plugin( Some(ParseError::NonUtf8(spans[1])) } } + 3 => { + let filename = working_set.get_span_contents(spans[1]); + let signature = working_set.get_span_contents(spans[2]); + + if let Ok(filename) = String::from_utf8(filename.to_vec()) { + if let Ok(signature) = serde_json::from_slice::(signature) { + let plugin_decl = PluginDeclaration::new(filename, signature); + working_set.add_plugin_decl(Box::new(plugin_decl)); + + None + } else { + Some(ParseError::PluginError( + "unable to deserialize signature".into(), + )) + } + } else { + Some(ParseError::NonUtf8(spans[1])) + } + } _ => { - let span = spans[2..].iter().fold(spans[2], |acc, next| Span { + let span = spans[3..].iter().fold(spans[3], |acc, next| Span { start: acc.start, end: next.end, }); diff --git a/crates/nu-plugin/src/plugin.rs b/crates/nu-plugin/src/plugin.rs index 9e5a68af5c..d6d44d2ab6 100644 --- a/crates/nu-plugin/src/plugin.rs +++ b/crates/nu-plugin/src/plugin.rs @@ -215,8 +215,8 @@ impl Command for PluginDeclaration { Ok(pipeline_data) } - fn is_plugin(&self) -> bool { - true + fn is_plugin(&self) -> Option<&str> { + Some(self.filename.as_str()) } } diff --git a/crates/nu-protocol/Cargo.toml b/crates/nu-protocol/Cargo.toml index 842aba468a..3656c0d456 100644 --- a/crates/nu-protocol/Cargo.toml +++ b/crates/nu-protocol/Cargo.toml @@ -12,4 +12,11 @@ serde = {version = "1.0.130", features = ["derive"]} chrono = { version="0.4.19", features=["serde"] } chrono-humanize = "0.2.1" byte-unit = "4.0.9" -im = "15.0.0" \ No newline at end of file +im = "15.0.0" +serde_json = { version = "1.0", optional = true } + +[features] +plugin = ["serde_json"] + +[dev-dependencies] +serde_json = "1.0" diff --git a/crates/nu-protocol/src/engine/command.rs b/crates/nu-protocol/src/engine/command.rs index 295898207e..7f7bad6109 100644 --- a/crates/nu-protocol/src/engine/command.rs +++ b/crates/nu-protocol/src/engine/command.rs @@ -47,8 +47,8 @@ pub trait Command: Send + Sync + CommandClone { } // Is a plugin command - fn is_plugin(&self) -> bool { - false + fn is_plugin(&self) -> Option<&str> { + None } // If command is a block i.e. def blah [] { }, get the block id diff --git a/crates/nu-protocol/src/engine/engine_state.rs b/crates/nu-protocol/src/engine/engine_state.rs index 9bf9f453ad..67128316c6 100644 --- a/crates/nu-protocol/src/engine/engine_state.rs +++ b/crates/nu-protocol/src/engine/engine_state.rs @@ -1,6 +1,7 @@ use super::Command; use crate::{ - ast::Block, BlockId, DeclId, Example, Overlay, OverlayId, Signature, Span, Type, VarId, + ast::Block, BlockId, DeclId, Example, Overlay, OverlayId, ShellError, Signature, Span, Type, + VarId, }; use core::panic; use std::{ @@ -8,6 +9,8 @@ use std::{ sync::{atomic::AtomicBool, Arc}, }; +#[cfg(feature = "plugin")] +use std::path::PathBuf; // Tells whether a decl etc. is visible or not #[derive(Debug, Clone)] struct Visibility { @@ -136,6 +139,8 @@ pub struct EngineState { overlays: im::Vector, pub scope: im::Vector, pub ctrlc: Option>, + #[cfg(feature = "plugin")] + pub plugin_signatures: Option, } pub const NU_VARIABLE_ID: usize = 0; @@ -154,6 +159,8 @@ impl EngineState { overlays: im::vector![], scope: im::vector![ScopeFrame::new()], ctrlc: None, + #[cfg(feature = "plugin")] + plugin_signatures: None, } } @@ -164,7 +171,7 @@ impl EngineState { /// /// When we want to preserve what the parser has created, we can take its output (the `StateDelta`) and /// use this function to merge it into the global state. - pub fn merge_delta(&mut self, mut delta: StateDelta) { + pub fn merge_delta(&mut self, mut delta: StateDelta) -> Result<(), ShellError> { // Take the mutable reference and extend the permanent state from the working set self.files.extend(delta.files); self.file_contents.extend(delta.file_contents); @@ -188,6 +195,53 @@ impl EngineState { last.overlays.insert(item.0, item.1); } last.visibility.merge_with(first.visibility); + + #[cfg(feature = "plugin")] + if !delta.plugin_decls.is_empty() { + for decl in delta.plugin_decls { + let name = decl.name().as_bytes().to_vec(); + self.decls.push_back(decl); + let decl_id = self.decls.len() - 1; + + last.decls.insert(name, decl_id); + last.visibility.use_decl_id(&decl_id); + } + + return self.update_plugin_file(); + } + } + + Ok(()) + } + + #[cfg(feature = "plugin")] + pub fn update_plugin_file(&self) -> Result<(), ShellError> { + use std::io::Write; + + // Updating the signatures plugin file with the added signatures + if let Some(plugin_path) = &self.plugin_signatures { + // Always creating the file which will erase previous signatures + let mut plugin_file = std::fs::File::create(plugin_path.as_path()) + .map_err(|err| ShellError::PluginError(err.to_string()))?; + + // Plugin definitions with parsed signature + for decl in self.plugin_decls() { + // A successful plugin registration already includes the plugin filename + // No need to check the None option + let file_name = decl.is_plugin().expect("plugin should have file name"); + + let line = serde_json::to_string_pretty(&decl.signature()) + .map(|signature| format!("register {} {}\n", file_name, signature)) + .map_err(|err| ShellError::PluginError(err.to_string()))?; + + plugin_file + .write_all(line.as_bytes()) + .map_err(|err| ShellError::PluginError(err.to_string()))?; + } + + Ok(()) + } else { + Err(ShellError::PluginError("Plugin file not found".into())) } } @@ -252,6 +306,10 @@ impl EngineState { None } + pub fn plugin_decls(&self) -> impl Iterator> { + self.decls.iter().filter(|decl| decl.is_plugin().is_some()) + } + pub fn find_overlay(&self, name: &[u8]) -> Option { for scope in self.scope.iter().rev() { if let Some(overlay_id) = scope.overlays.get(name) { @@ -314,7 +372,7 @@ impl EngineState { output } - pub fn get_signatures_with_examples(&self) -> Vec<(Signature, Vec)> { + pub fn get_signatures_with_examples(&self) -> Vec<(Signature, Vec, bool)> { let mut output = vec![]; for decl in self.decls.iter() { if decl.get_block_id().is_none() { @@ -322,7 +380,7 @@ impl EngineState { signature.usage = decl.usage().to_string(); signature.extra_usage = decl.extra_usage().to_string(); - output.push((signature, decl.examples())); + output.push((signature, decl.examples(), decl.is_plugin().is_some())); } } @@ -418,6 +476,8 @@ pub struct StateDelta { pub(crate) file_contents: Vec<(Vec, usize, usize)>, vars: Vec, // indexed by VarId decls: Vec>, // indexed by DeclId + #[cfg(feature = "plugin")] + plugin_decls: Vec>, // indexed by DeclId blocks: Vec, // indexed by BlockId overlays: Vec, // indexed by OverlayId pub scope: Vec, @@ -457,6 +517,8 @@ impl<'a> StateWorkingSet<'a> { file_contents: vec![], vars: vec![], decls: vec![], + #[cfg(feature = "plugin")] + plugin_decls: vec![], blocks: vec![], overlays: vec![], scope: vec![ScopeFrame::new()], @@ -527,6 +589,11 @@ impl<'a> StateWorkingSet<'a> { scope_frame.predecls.insert(name, decl_id) } + #[cfg(feature = "plugin")] + pub fn add_plugin_decl(&mut self, decl: Box) { + self.delta.plugin_decls.push(decl); + } + pub fn merge_predecl(&mut self, name: &[u8]) -> Option { let scope_frame = self .delta diff --git a/crates/nu-protocol/src/signature.rs b/crates/nu-protocol/src/signature.rs index 43d3491f52..44b564a52f 100644 --- a/crates/nu-protocol/src/signature.rs +++ b/crates/nu-protocol/src/signature.rs @@ -1,3 +1,6 @@ +use serde::Deserialize; +use serde::Serialize; + use crate::ast::Call; use crate::engine::Command; use crate::engine::EngineState; @@ -7,7 +10,7 @@ use crate::PipelineData; use crate::SyntaxShape; use crate::VarId; -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Flag { pub long: String, pub short: Option, @@ -18,7 +21,7 @@ pub struct Flag { pub var_id: Option, } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct PositionalArg { pub name: String, pub desc: String, @@ -27,7 +30,7 @@ pub struct PositionalArg { pub var_id: Option, } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum Category { Default, Conversions, @@ -66,7 +69,7 @@ impl std::fmt::Display for Category { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct Signature { pub name: String, pub usage: String, diff --git a/crates/nu-protocol/src/syntax_shape.rs b/crates/nu-protocol/src/syntax_shape.rs index ca442eae58..155c6ce96b 100644 --- a/crates/nu-protocol/src/syntax_shape.rs +++ b/crates/nu-protocol/src/syntax_shape.rs @@ -1,7 +1,9 @@ +use serde::{Deserialize, Serialize}; + use crate::Type; /// The syntactic shapes that values must match to be passed into a command. You can think of this as the type-checking that occurs when you call a function. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum SyntaxShape { /// A specific match to a word or symbol Keyword(Vec, Box), diff --git a/crates/nu-protocol/tests/test_signature.rs b/crates/nu-protocol/tests/test_signature.rs index 5559d7e593..3a9048824f 100644 --- a/crates/nu-protocol/tests/test_signature.rs +++ b/crates/nu-protocol/tests/test_signature.rs @@ -120,3 +120,51 @@ fn test_signature_same_name() { ) .named("name", SyntaxShape::String, "named description", Some('n')); } + +#[test] +fn test_signature_round_trip() { + let signature = Signature::new("new_signature") + .desc("description") + .required("first", SyntaxShape::String, "first required") + .required("second", SyntaxShape::Int, "second required") + .optional("optional", SyntaxShape::String, "optional description") + .required_named( + "req_named", + SyntaxShape::String, + "required named description", + Some('r'), + ) + .named("named", SyntaxShape::String, "named description", Some('n')) + .switch("switch", "switch description", None) + .rest("rest", SyntaxShape::String, "rest description") + .category(nu_protocol::Category::Conversions); + + let string = serde_json::to_string_pretty(&signature).unwrap(); + let returned: Signature = serde_json::from_str(&string).unwrap(); + + assert_eq!(signature.name, returned.name); + assert_eq!(signature.usage, returned.usage); + assert_eq!(signature.extra_usage, returned.extra_usage); + assert_eq!(signature.is_filter, returned.is_filter); + assert_eq!(signature.category, returned.category); + + signature + .required_positional + .iter() + .zip(returned.required_positional.iter()) + .for_each(|(lhs, rhs)| assert_eq!(lhs, rhs)); + + signature + .optional_positional + .iter() + .zip(returned.optional_positional.iter()) + .for_each(|(lhs, rhs)| assert_eq!(lhs, rhs)); + + signature + .named + .iter() + .zip(returned.named.iter()) + .for_each(|(lhs, rhs)| assert_eq!(lhs, rhs)); + + assert_eq!(signature.rest_positional, returned.rest_positional,); +} diff --git a/src/main.rs b/src/main.rs index c54c9ccf36..ce096c1d4d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -118,7 +118,10 @@ fn main() -> Result<()> { (output, working_set.render()) }; - engine_state.merge_delta(delta); + if let Err(err) = engine_state.merge_delta(delta) { + let working_set = StateWorkingSet::new(&engine_state); + report_error(&working_set, &err); + } let mut stack = nu_protocol::engine::Stack::new(); @@ -185,10 +188,9 @@ fn main() -> Result<()> { config_path.push("nushell"); config_path.push("config.nu"); - // FIXME: remove this message when we're ready - println!("Loading config from: {:?}", config_path); - if config_path.exists() { + // FIXME: remove this message when we're ready + println!("Loading config from: {:?}", config_path); let config_filename = config_path.to_string_lossy().to_owned(); if let Ok(contents) = std::fs::read_to_string(&config_path) { @@ -206,6 +208,24 @@ fn main() -> Result<()> { None }; + #[cfg(feature = "plugin")] + { + // Reading signatures from signature file + // The plugin.nu file stores the parsed signature collected from each registered plugin + if let Some(mut plugin_path) = nu_path::config_dir() { + // Path to store plugins signatures + plugin_path.push("nushell"); + plugin_path.push("plugin.nu"); + engine_state.plugin_signatures = Some(plugin_path.clone()); + + let plugin_filename = plugin_path.to_string_lossy().to_owned(); + + if let Ok(contents) = std::fs::read_to_string(&plugin_path) { + eval_source(&mut engine_state, &mut stack, &contents, &plugin_filename); + } + } + } + loop { //Reset the ctrl-c handler ctrlc.store(false, Ordering::SeqCst); @@ -387,7 +407,10 @@ fn eval_source( (output, working_set.render()) }; - engine_state.merge_delta(delta); + if let Err(err) = engine_state.merge_delta(delta) { + let working_set = StateWorkingSet::new(engine_state); + report_error(&working_set, &err); + } match eval_block( engine_state,