Introduce new command - nu check (#5864)

* nu check command - 1

* Support stream

* Polish code and fix corner case
This commit is contained in:
Kangaxx-0 2022-06-26 04:53:06 -07:00 committed by GitHub
parent 32f0f94b46
commit dc9d939c83
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 858 additions and 0 deletions

View file

@ -158,6 +158,7 @@ pub fn create_default_context(cwd: impl AsRef<Path>) -> EngineState {
Complete,
Exec,
External,
NuCheck,
Ps,
Sys,
};

View file

@ -1,6 +1,7 @@
mod benchmark;
mod complete;
mod exec;
mod nu_check;
mod ps;
mod run_external;
mod sys;
@ -9,6 +10,7 @@ mod which_;
pub use benchmark::Benchmark;
pub use complete::Complete;
pub use exec::Exec;
pub use nu_check::NuCheck;
pub use ps::Ps;
pub use run_external::{External, ExternalCommand};
pub use sys::Sys;

View file

@ -0,0 +1,302 @@
use nu_engine::{current_dir, CallExt};
use nu_parser::{parse, parse_module_block, unescape_unquote_string};
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack, StateWorkingSet};
use nu_protocol::{
Category, Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Value,
};
#[derive(Clone)]
pub struct NuCheck;
impl Command for NuCheck {
fn name(&self) -> &str {
"nu-check"
}
fn signature(&self) -> Signature {
Signature::build("nu-check")
.optional("path", SyntaxShape::Filepath, "File path to parse")
.switch("as-module", "Parse content as module", Some('m'))
.switch("debug", "Show error messages", Some('d'))
.category(Category::Strings)
}
fn usage(&self) -> &str {
"Validate and parse input content"
}
fn search_terms(&self) -> Vec<&str> {
vec!["syntax", "parse", "debug"]
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let path: Option<Spanned<String>> = call.opt(engine_state, stack, 0)?;
let is_module = call.has_flag("as-module");
let is_debug = call.has_flag("debug");
let config = engine_state.get_config();
let mut contents = vec![];
// DO NOT ever try to merge the working_set in this command
let mut working_set = StateWorkingSet::new(engine_state);
match input {
PipelineData::Value(Value::String { val, span }, ..) => {
let contents = Vec::from(val);
if is_module {
parse_module(&mut working_set, None, contents, is_debug, span)
} else {
parse_script(&mut working_set, None, contents, is_debug, span)
}
}
PipelineData::ListStream(stream, ..) => {
let list_stream = stream.into_string("\n", config);
let contents = Vec::from(list_stream);
if is_module {
parse_module(&mut working_set, None, contents, is_debug, call.head)
} else {
parse_script(&mut working_set, None, contents, is_debug, call.head)
}
}
PipelineData::ExternalStream {
stdout: Some(stream),
..
} => {
let raw_stream: Vec<_> = stream.stream.into_iter().collect();
for r in raw_stream {
match r {
Ok(v) => contents.extend(v),
Err(error) => return Err(error),
};
}
if is_module {
parse_module(&mut working_set, None, contents, is_debug, call.head)
} else {
parse_script(&mut working_set, None, contents, is_debug, call.head)
}
}
_ => {
if path.is_some() {
let path = match find_path(path, engine_state, stack, call.head) {
Ok(path) => path,
Err(error) => return Err(error),
};
let ext: Vec<_> = path.rsplitn(2, '.').collect();
if ext[0] != "nu" {
return Err(ShellError::GenericError(
"Cannot parse input".to_string(),
"File extension must be the type of .nu".to_string(),
Some(call.head),
None,
Vec::new(),
));
}
if is_module {
parse_file_module(path, &mut working_set, call, is_debug)
} else {
parse_file_script(path, &mut working_set, call, is_debug)
}
} else {
Err(ShellError::GenericError(
"Failed to execute command".to_string(),
"Please run 'nu-check --help' for more details".to_string(),
Some(call.head),
None,
Vec::new(),
))
}
}
}
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Parse a input file as script(Default)",
example: "nu-check script.nu",
result: None,
},
Example {
description: "Parse a input file as module",
example: "nu-check --as-module module.nu",
result: None,
},
Example {
description: "Parse a input file by showing error message",
example: "nu-check -d script.nu",
result: None,
},
Example {
description: "Parse an external stream as script by showing error message",
example: "open foo.nu | nu-check -d script.nu",
result: None,
},
Example {
description: "Parse an internal stream as module by showing error message",
example: "open module.nu | lines | nu-check -d --as-module module.nu",
result: None,
},
Example {
description: "Parse a string as script",
example: "echo $'two(char nl)lines' | nu-check ",
result: None,
},
]
}
}
fn find_path(
path: Option<Spanned<String>>,
engine_state: &EngineState,
stack: &mut Stack,
span: Span,
) -> Result<String, ShellError> {
let cwd = current_dir(engine_state, stack)?;
let path = match path {
Some(s) => {
let path_no_whitespace = &s.item.trim_end_matches(|x| matches!(x, '\x09'..='\x0d'));
let path = match nu_path::canonicalize_with(path_no_whitespace, &cwd) {
Ok(p) => {
if !p.is_file() {
return Err(ShellError::GenericError(
"Cannot parse input".to_string(),
"Path is not a file".to_string(),
Some(s.span),
None,
Vec::new(),
));
} else {
p
}
}
Err(_) => {
return Err(ShellError::FileNotFound(s.span));
}
};
path.to_string_lossy().to_string()
}
None => {
return Err(ShellError::NotFound(span));
}
};
Ok(path)
}
fn parse_module(
working_set: &mut StateWorkingSet,
filename: Option<String>,
contents: Vec<u8>,
is_debug: bool,
span: Span,
) -> Result<PipelineData, ShellError> {
let start = working_set.next_span_start();
working_set.add_file(
filename.unwrap_or_else(|| "empty".to_string()),
contents.as_ref(),
);
let end = working_set.next_span_start();
let new_span = Span::new(start, end);
let (_, _, err) = parse_module_block(working_set, new_span, &[]);
if err.is_some() {
if is_debug {
let msg = format!(
r#"Found : {}"#,
err.expect("Unable to parse content as module")
);
Err(ShellError::GenericError(
"Failed to parse content".to_string(),
msg,
Some(span),
Some("If the content is intended to be a script, please try to remove `--as-module` flag ".to_string()),
Vec::new(),
))
} else {
Ok(PipelineData::Value(Value::boolean(false, new_span), None))
}
} else {
Ok(PipelineData::Value(Value::boolean(true, new_span), None))
}
}
fn parse_script(
working_set: &mut StateWorkingSet,
filename: Option<&str>,
contents: Vec<u8>,
is_debug: bool,
span: Span,
) -> Result<PipelineData, ShellError> {
let (_, err) = parse(working_set, filename, &contents, false, &[]);
if err.is_some() {
let msg = format!(r#"Found : {}"#, err.expect("Unable to parse content"));
if is_debug {
Err(ShellError::GenericError(
"Failed to parse content".to_string(),
msg,
Some(span),
Some("If the content is intended to be a module, please consider flag of `--as-module` ".to_string()),
Vec::new(),
))
} else {
Ok(PipelineData::Value(Value::boolean(false, span), None))
}
} else {
Ok(PipelineData::Value(Value::boolean(true, span), None))
}
}
fn parse_file_script(
path: String,
working_set: &mut StateWorkingSet,
call: &Call,
is_debug: bool,
) -> Result<PipelineData, ShellError> {
let (filename, err) = unescape_unquote_string(path.as_bytes(), call.head);
if err.is_none() {
if let Ok(contents) = std::fs::read(&path) {
parse_script(
working_set,
Some(filename.as_str()),
contents,
is_debug,
call.head,
)
} else {
Err(ShellError::IOError("Can not read path".to_string()))
}
} else {
Err(ShellError::NotFound(call.head))
}
}
fn parse_file_module(
path: String,
working_set: &mut StateWorkingSet,
call: &Call,
is_debug: bool,
) -> Result<PipelineData, ShellError> {
let (filename, err) = unescape_unquote_string(path.as_bytes(), call.head);
if err.is_none() {
if let Ok(contents) = std::fs::read(path) {
parse_module(working_set, Some(filename), contents, is_debug, call.head)
} else {
Err(ShellError::IOError("Can not read path".to_string()))
}
} else {
Err(ShellError::NotFound(call.head))
}
}

View file

@ -38,6 +38,7 @@ mod merge;
mod mkdir;
mod move_;
mod network;
mod nu_check;
mod open;
mod parse;
mod path;

View file

@ -0,0 +1,552 @@
use nu_test_support::fs::Stub::FileWithContentToBeTrimmed;
use nu_test_support::playground::Playground;
use nu_test_support::{nu, pipeline};
#[test]
fn parse_script_success() {
Playground::setup("nu_check_test_1", |dirs, sandbox| {
sandbox.with_files(vec![FileWithContentToBeTrimmed(
"script.nu",
r#"
greet "world"
def greet [name] {
echo "hello" $name
}
"#,
)]);
let actual = nu!(
cwd: dirs.test(), pipeline(
r#"
nu-check script.nu
"#
));
assert!(actual.err.is_empty());
})
}
#[test]
fn parse_script_with_wrong_type() {
Playground::setup("nu_check_test_2", |dirs, sandbox| {
sandbox.with_files(vec![FileWithContentToBeTrimmed(
"script.nu",
r#"
greet "world"
def greet [name] {
echo "hello" $name
}
"#,
)]);
let actual = nu!(
cwd: dirs.test(), pipeline(
r#"
nu-check -d --as-module script.nu
"#
));
assert!(actual.err.contains("Failed to parse content"));
})
}
#[test]
fn parse_script_failure() {
Playground::setup("nu_check_test_3", |dirs, sandbox| {
sandbox.with_files(vec![FileWithContentToBeTrimmed(
"script.nu",
r#"
greet "world"
def greet [name {
echo "hello" $name
}
"#,
)]);
let actual = nu!(
cwd: dirs.test(), pipeline(
r#"
nu-check -d script.nu
"#
));
assert!(actual.err.contains("Unexpected end of code"));
})
}
#[test]
fn parse_module_success() {
Playground::setup("nu_check_test_4", |dirs, sandbox| {
sandbox.with_files(vec![FileWithContentToBeTrimmed(
"foo.nu",
r#"
# foo.nu
export def hello [name: string] {
$"hello ($name)!"
}
export def hi [where: string] {
$"hi ($where)!"
}
"#,
)]);
let actual = nu!(
cwd: dirs.test(), pipeline(
r#"
nu-check --as-module foo.nu
"#
));
assert!(actual.err.is_empty());
})
}
#[test]
fn parse_module_with_wrong_type() {
Playground::setup("nu_check_test_5", |dirs, sandbox| {
sandbox.with_files(vec![FileWithContentToBeTrimmed(
"foo.nu",
r#"
# foo.nu
export def hello [name: string {
$"hello ($name)!"
}
export def hi [where: string] {
$"hi ($where)!"
}
"#,
)]);
let actual = nu!(
cwd: dirs.test(), pipeline(
r#"
nu-check -d foo.nu
"#
));
assert!(actual.err.contains("Failed to parse content"));
})
}
#[test]
fn parse_module_failure() {
Playground::setup("nu_check_test_6", |dirs, sandbox| {
sandbox.with_files(vec![FileWithContentToBeTrimmed(
"foo.nu",
r#"
# foo.nu
export def hello [name: string {
$"hello ($name)!"
}
export def hi [where: string] {
$"hi ($where)!"
}
"#,
)]);
let actual = nu!(
cwd: dirs.test(), pipeline(
r#"
nu-check -d --as-module foo.nu
"#
));
assert!(actual.err.contains("Unexpected end of code"));
})
}
#[test]
fn file_not_exist() {
Playground::setup("nu_check_test_7", |dirs, _sandbox| {
let actual = nu!(
cwd: dirs.test(), pipeline(
r#"
nu-check --as-module foo.nu
"#
));
assert!(actual.err.contains("file not found"));
})
}
#[test]
fn parse_unsupported_file() {
Playground::setup("nu_check_test_8", |dirs, sandbox| {
sandbox.with_files(vec![FileWithContentToBeTrimmed(
"foo.txt",
r#"
# foo.nu
export def hello [name: string {
$"hello ($name)!"
}
export def hi [where: string] {
$"hi ($where)!"
}
"#,
)]);
let actual = nu!(
cwd: dirs.test(), pipeline(
r#"
nu-check --as-module foo.txt
"#
));
assert!(actual
.err
.contains("File extension must be the type of .nu"));
})
}
#[test]
fn parse_dir_failure() {
Playground::setup("nu_check_test_9", |dirs, _sandbox| {
let actual = nu!(
cwd: dirs.test(), pipeline(
r#"
nu-check --as-module ~
"#
));
assert!(actual.err.contains("Path is not a file"));
})
}
#[test]
fn parse_module_success_2() {
Playground::setup("nu_check_test_10", |dirs, sandbox| {
sandbox.with_files(vec![FileWithContentToBeTrimmed(
"foo.nu",
r#"
# foo.nu
export env MYNAME { "Arthur, King of the Britons" }
"#,
)]);
let actual = nu!(
cwd: dirs.test(), pipeline(
r#"
nu-check --as-module foo.nu
"#
));
assert!(actual.err.is_empty());
})
}
#[test]
fn parse_script_success_with_raw_stream() {
Playground::setup("nu_check_test_11", |dirs, sandbox| {
sandbox.with_files(vec![FileWithContentToBeTrimmed(
"script.nu",
r#"
greet "world"
def greet [name] {
echo "hello" $name
}
"#,
)]);
let actual = nu!(
cwd: dirs.test(), pipeline(
r#"
open script.nu | nu-check
"#
));
assert!(actual.err.is_empty());
})
}
#[test]
fn parse_module_success_with_raw_stream() {
Playground::setup("nu_check_test_12", |dirs, sandbox| {
sandbox.with_files(vec![FileWithContentToBeTrimmed(
"foo.nu",
r#"
# foo.nu
export def hello [name: string] {
$"hello ($name)!"
}
export def hi [where: string] {
$"hi ($where)!"
}
"#,
)]);
let actual = nu!(
cwd: dirs.test(), pipeline(
r#"
open foo.nu | nu-check --as-module
"#
));
assert!(actual.err.is_empty());
})
}
#[test]
fn parse_string_as_script_success() {
Playground::setup("nu_check_test_13", |dirs, _sandbox| {
let actual = nu!(
cwd: dirs.test(), pipeline(
r#"
echo $'two(char nl)lines' | nu-check
"#
));
assert!(actual.err.is_empty());
})
}
#[test]
fn parse_string_as_script() {
Playground::setup("nu_check_test_14", |dirs, _sandbox| {
let actual = nu!(
cwd: dirs.test(), pipeline(
r#"
echo $'two(char nl)lines' | nu-check -d --as-module
"#
));
println!("the out put is {}", actual.err);
assert!(actual.err.contains("Failed to parse content"));
})
}
#[test]
fn parse_module_success_with_internal_stream() {
Playground::setup("nu_check_test_15", |dirs, sandbox| {
sandbox.with_files(vec![FileWithContentToBeTrimmed(
"foo.nu",
r#"
# foo.nu
export def hello [name: string] {
$"hello ($name)!"
}
export def hi [where: string] {
$"hi ($where)!"
}
"#,
)]);
let actual = nu!(
cwd: dirs.test(), pipeline(
r#"
open foo.nu | lines | nu-check --as-module
"#
));
assert!(actual.err.is_empty());
})
}
#[test]
fn parse_script_success_with_complex_internal_stream() {
Playground::setup("nu_check_test_16", |dirs, sandbox| {
sandbox.with_files(vec![FileWithContentToBeTrimmed(
"grep.nu",
r#"
#grep for nu
def grep-nu [
search #search term
entrada? #file or pipe
#
#Examples
#grep-nu search file.txt
#ls **/* | some_filter | grep-nu search
#open file.txt | grep-nu search
] {
if ($entrada | empty?) {
if ($in | column? name) {
grep -ihHn $search ($in | get name)
} else {
($in | into string) | grep -ihHn $search
}
} else {
grep -ihHn $search $entrada
}
| lines
| parse "{file}:{line}:{match}"
| str trim
| update match {|f|
$f.match
| nu-highlight
}
| rename "source file" "line number"
}
"#,
)]);
let actual = nu!(
cwd: dirs.test(), pipeline(
r#"
open grep.nu | lines | nu-check
"#
));
assert!(actual.err.is_empty());
})
}
#[test]
fn parse_script_failure_with_complex_internal_stream() {
Playground::setup("nu_check_test_17", |dirs, sandbox| {
sandbox.with_files(vec![FileWithContentToBeTrimmed(
"grep.nu",
r#"
#grep for nu
def grep-nu [
search #search term
entrada? #file or pipe
#
#Examples
#grep-nu search file.txt
#ls **/* | some_filter | grep-nu search
#open file.txt | grep-nu search
]
if ($entrada | empty?) {
if ($in | column? name) {
grep -ihHn $search ($in | get name)
} else {
($in | into string) | grep -ihHn $search
}
} else {
grep -ihHn $search $entrada
}
| lines
| parse "{file}:{line}:{match}"
| str trim
| update match {|f|
$f.match
| nu-highlight
}
| rename "source file" "line number"
}
"#,
)]);
let actual = nu!(
cwd: dirs.test(), pipeline(
r#"
open grep.nu | lines | nu-check
"#
));
assert_eq!(actual.out, "false".to_string());
})
}
#[test]
fn parse_script_success_with_complex_external_stream() {
Playground::setup("nu_check_test_18", |dirs, sandbox| {
sandbox.with_files(vec![FileWithContentToBeTrimmed(
"grep.nu",
r#"
#grep for nu
def grep-nu [
search #search term
entrada? #file or pipe
#
#Examples
#grep-nu search file.txt
#ls **/* | some_filter | grep-nu search
#open file.txt | grep-nu search
] {
if ($entrada | empty?) {
if ($in | column? name) {
grep -ihHn $search ($in | get name)
} else {
($in | into string) | grep -ihHn $search
}
} else {
grep -ihHn $search $entrada
}
| lines
| parse "{file}:{line}:{match}"
| str trim
| update match {|f|
$f.match
| nu-highlight
}
| rename "source file" "line number"
}
"#,
)]);
let actual = nu!(
cwd: dirs.test(), pipeline(
r#"
open grep.nu | nu-check
"#
));
assert!(actual.err.is_empty());
})
}
#[test]
fn parse_module_success_with_complex_external_stream() {
Playground::setup("nu_check_test_19", |dirs, sandbox| {
sandbox.with_files(vec![FileWithContentToBeTrimmed(
"grep.nu",
r#"
#grep for nu
def grep-nu [
search #search term
entrada? #file or pipe
#
#Examples
#grep-nu search file.txt
#ls **/* | some_filter | grep-nu search
#open file.txt | grep-nu search
] {
if ($entrada | empty?) {
if ($in | column? name) {
grep -ihHn $search ($in | get name)
} else {
($in | into string) | grep -ihHn $search
}
} else {
grep -ihHn $search $entrada
}
| lines
| parse "{file}:{line}:{match}"
| str trim
| update match {|f|
$f.match
| nu-highlight
}
| rename "source file" "line number"
}
"#,
)]);
let actual = nu!(
cwd: dirs.test(), pipeline(
r#"
open grep.nu | nu-check -d --as-module
"#
));
assert!(actual.err.is_empty());
})
}