Make parsing for unknown args in known externals like normal external calls

This commit is contained in:
Devyn Cairns 2024-07-19 18:11:46 -07:00
parent dbd60ed4f4
commit 993c7c09d4
No known key found for this signature in database
6 changed files with 140 additions and 15 deletions

View File

@ -5,7 +5,7 @@ use nu_protocol::{
RecordItem, RecordItem,
}, },
engine::StateWorkingSet, engine::StateWorkingSet,
DeclId, Span, VarId, DeclId, Span, SyntaxShape, VarId,
}; };
use std::fmt::{Display, Formatter, Result}; use std::fmt::{Display, Formatter, Result};
@ -166,6 +166,22 @@ fn flatten_pipeline_element_into(
} }
} }
fn flatten_positional_arg_into(
working_set: &StateWorkingSet,
positional: &Expression,
shape: &SyntaxShape,
output: &mut Vec<(Span, FlatShape)>,
) {
if matches!(shape, SyntaxShape::ExternalArgument)
&& matches!(positional.expr, Expr::String(..) | Expr::GlobPattern(..))
{
// Make known external arguments look more like external arguments
output.push((positional.span, FlatShape::ExternalArg));
} else {
flatten_expression_into(working_set, positional, output)
}
}
fn flatten_expression_into( fn flatten_expression_into(
working_set: &StateWorkingSet, working_set: &StateWorkingSet,
expr: &Expression, expr: &Expression,
@ -249,16 +265,40 @@ fn flatten_expression_into(
} }
} }
Expr::Call(call) => { Expr::Call(call) => {
let decl = working_set.get_decl(call.decl_id);
if call.head.end != 0 { if call.head.end != 0 {
// Make sure we don't push synthetic calls // Make sure we don't push synthetic calls
output.push((call.head, FlatShape::InternalCall(call.decl_id))); output.push((call.head, FlatShape::InternalCall(call.decl_id)));
} }
// Follow positional arguments from the signature.
let signature = decl.signature();
let mut positional_args = signature
.required_positional
.iter()
.chain(&signature.optional_positional);
let arg_start = output.len(); let arg_start = output.len();
for arg in &call.arguments { for arg in &call.arguments {
match arg { match arg {
Argument::Positional(positional) | Argument::Unknown(positional) => { Argument::Positional(positional) => {
flatten_expression_into(working_set, positional, output) let positional_arg = positional_args.next();
let shape = positional_arg
.or(signature.rest_positional.as_ref())
.map(|arg| &arg.shape)
.unwrap_or(&SyntaxShape::Any);
flatten_positional_arg_into(working_set, positional, shape, output)
}
Argument::Unknown(positional) => {
let shape = signature
.rest_positional
.as_ref()
.map(|arg| &arg.shape)
.unwrap_or(&SyntaxShape::Any);
flatten_positional_arg_into(working_set, positional, shape, output)
} }
Argument::Named(named) => { Argument::Named(named) => {
if named.0.span.end != 0 { if named.0.span.end != 0 {

View File

@ -783,6 +783,16 @@ pub fn parse_extern(
working_set.get_block_mut(block_id).signature = signature; working_set.get_block_mut(block_id).signature = signature;
} }
} else { } else {
if signature.rest_positional.is_none() {
// Make sure that a known external takes rest args with ExternalArgument
// shape
*signature = signature.rest(
"args",
SyntaxShape::ExternalArgument,
"all other arguments to the command",
);
}
let decl = KnownExternal { let decl = KnownExternal {
name: external_name, name: external_name,
usage, usage,

View File

@ -221,6 +221,22 @@ pub(crate) fn check_call(
} }
} }
/// Parses an unknown argument for the given signature. This handles the parsing as appropriate to
/// the rest type of the command.
fn parse_unknown_arg(
working_set: &mut StateWorkingSet,
span: Span,
signature: &Signature,
) -> Expression {
let shape = signature
.rest_positional
.as_ref()
.map(|arg| arg.shape.clone())
.unwrap_or(SyntaxShape::Any);
parse_value(working_set, span, &shape)
}
/// Parses a string in the arg or head position of an external call. /// Parses a string in the arg or head position of an external call.
/// ///
/// If the string begins with `r#`, it is parsed as a raw string. If it doesn't contain any quotes /// If the string begins with `r#`, it is parsed as a raw string. If it doesn't contain any quotes
@ -427,11 +443,7 @@ fn parse_external_string(working_set: &mut StateWorkingSet, span: Span) -> Expre
fn parse_external_arg(working_set: &mut StateWorkingSet, span: Span) -> ExternalArgument { fn parse_external_arg(working_set: &mut StateWorkingSet, span: Span) -> ExternalArgument {
let contents = working_set.get_span_contents(span); let contents = working_set.get_span_contents(span);
if contents.starts_with(b"$") || contents.starts_with(b"(") { if contents.len() > 3
ExternalArgument::Regular(parse_dollar_expr(working_set, span))
} else if contents.starts_with(b"[") {
ExternalArgument::Regular(parse_list_expression(working_set, span, &SyntaxShape::Any))
} else if contents.len() > 3
&& contents.starts_with(b"...") && contents.starts_with(b"...")
&& (contents[3] == b'$' || contents[3] == b'[' || contents[3] == b'(') && (contents[3] == b'$' || contents[3] == b'[' || contents[3] == b'(')
{ {
@ -441,7 +453,19 @@ fn parse_external_arg(working_set: &mut StateWorkingSet, span: Span) -> External
&SyntaxShape::List(Box::new(SyntaxShape::Any)), &SyntaxShape::List(Box::new(SyntaxShape::Any)),
)) ))
} else { } else {
ExternalArgument::Regular(parse_external_string(working_set, span)) ExternalArgument::Regular(parse_regular_external_arg(working_set, span))
}
}
fn parse_regular_external_arg(working_set: &mut StateWorkingSet, span: Span) -> Expression {
let contents = working_set.get_span_contents(span);
if contents.starts_with(b"$") || contents.starts_with(b"(") {
parse_dollar_expr(working_set, span)
} else if contents.starts_with(b"[") {
parse_list_expression(working_set, span, &SyntaxShape::Any)
} else {
parse_external_string(working_set, span)
} }
} }
@ -998,7 +1022,7 @@ pub fn parse_internal_call(
&& signature.allows_unknown_args && signature.allows_unknown_args
{ {
working_set.parse_errors.truncate(starting_error_count); working_set.parse_errors.truncate(starting_error_count);
let arg = parse_value(working_set, arg_span, &SyntaxShape::Any); let arg = parse_unknown_arg(working_set, arg_span, &signature);
call.add_unknown(arg); call.add_unknown(arg);
} else { } else {
@ -1040,7 +1064,7 @@ pub fn parse_internal_call(
&& signature.allows_unknown_args && signature.allows_unknown_args
{ {
working_set.parse_errors.truncate(starting_error_count); working_set.parse_errors.truncate(starting_error_count);
let arg = parse_value(working_set, arg_span, &SyntaxShape::Any); let arg = parse_unknown_arg(working_set, arg_span, &signature);
call.add_unknown(arg); call.add_unknown(arg);
} else { } else {
@ -1196,7 +1220,7 @@ pub fn parse_internal_call(
call.add_positional(arg); call.add_positional(arg);
positional_idx += 1; positional_idx += 1;
} else if signature.allows_unknown_args { } else if signature.allows_unknown_args {
let arg = parse_value(working_set, arg_span, &SyntaxShape::Any); let arg = parse_unknown_arg(working_set, arg_span, &signature);
call.add_unknown(arg); call.add_unknown(arg);
} else { } else {
@ -4670,7 +4694,8 @@ pub fn parse_value(
| SyntaxShape::Signature | SyntaxShape::Signature
| SyntaxShape::Filepath | SyntaxShape::Filepath
| SyntaxShape::String | SyntaxShape::String
| SyntaxShape::GlobPattern => {} | SyntaxShape::GlobPattern
| SyntaxShape::ExternalArgument => {}
_ => { _ => {
working_set.error(ParseError::Expected("non-[] value", span)); working_set.error(ParseError::Expected("non-[] value", span));
return Expression::garbage(working_set, span); return Expression::garbage(working_set, span);
@ -4747,6 +4772,8 @@ pub fn parse_value(
Expression::garbage(working_set, span) Expression::garbage(working_set, span)
} }
SyntaxShape::ExternalArgument => parse_regular_external_arg(working_set, span),
SyntaxShape::Any => { SyntaxShape::Any => {
if bytes.starts_with(b"[") { if bytes.starts_with(b"[") {
//parse_value(working_set, span, &SyntaxShape::Table) //parse_value(working_set, span, &SyntaxShape::Table)

View File

@ -47,6 +47,12 @@ pub enum SyntaxShape {
/// A general expression, eg `1 + 2` or `foo --bar` /// A general expression, eg `1 + 2` or `foo --bar`
Expression, Expression,
/// A (typically) string argument that follows external command argument parsing rules.
///
/// Filepaths are expanded if unquoted, globs are allowed, and quotes embedded within unknown
/// args are unquoted.
ExternalArgument,
/// A filepath is allowed /// A filepath is allowed
Filepath, Filepath,
@ -145,6 +151,7 @@ impl SyntaxShape {
SyntaxShape::DateTime => Type::Date, SyntaxShape::DateTime => Type::Date,
SyntaxShape::Duration => Type::Duration, SyntaxShape::Duration => Type::Duration,
SyntaxShape::Expression => Type::Any, SyntaxShape::Expression => Type::Any,
SyntaxShape::ExternalArgument => Type::Any,
SyntaxShape::Filepath => Type::String, SyntaxShape::Filepath => Type::String,
SyntaxShape::Directory => Type::String, SyntaxShape::Directory => Type::String,
SyntaxShape::Float => Type::Float, SyntaxShape::Float => Type::Float,
@ -238,6 +245,7 @@ impl Display for SyntaxShape {
SyntaxShape::Signature => write!(f, "signature"), SyntaxShape::Signature => write!(f, "signature"),
SyntaxShape::MatchBlock => write!(f, "match-block"), SyntaxShape::MatchBlock => write!(f, "match-block"),
SyntaxShape::Expression => write!(f, "expression"), SyntaxShape::Expression => write!(f, "expression"),
SyntaxShape::ExternalArgument => write!(f, "external-argument"),
SyntaxShape::Boolean => write!(f, "bool"), SyntaxShape::Boolean => write!(f, "bool"),
SyntaxShape::Error => write!(f, "error"), SyntaxShape::Error => write!(f, "error"),
SyntaxShape::CompleterWrapper(x, _) => write!(f, "completable<{x}>"), SyntaxShape::CompleterWrapper(x, _) => write!(f, "completable<{x}>"),

View File

@ -186,8 +186,12 @@ fn help_present_in_def() -> TestResult {
#[test] #[test]
fn help_not_present_in_extern() -> TestResult { fn help_not_present_in_extern() -> TestResult {
run_test( run_test(
"module test {export extern \"git fetch\" []}; use test `git fetch`; help git fetch | ansi strip", r#"
"Usage:\n > git fetch", module test {export extern "git fetch" []};
use test `git fetch`;
help git fetch | find help | to text | ansi strip
"#,
"",
) )
} }

View File

@ -137,3 +137,39 @@ fn known_external_aliased_subcommand_from_module() -> TestResult {
String::from_utf8(output.stdout)?.trim(), String::from_utf8(output.stdout)?.trim(),
) )
} }
#[test]
fn known_external_arg_expansion() -> TestResult {
run_test(
r#"
extern echo [];
echo ~/foo
"#,
&dirs::home_dir()
.expect("can't find home dir")
.join("foo")
.to_string_lossy(),
)
}
#[test]
fn known_external_arg_quoted_no_expand() -> TestResult {
run_test(
r#"
extern echo [];
echo "~/foo"
"#,
"~/foo",
)
}
#[test]
fn known_external_arg_internally_quoted_options() -> TestResult {
run_test(
r#"
extern echo [];
echo --option="test"
"#,
"--option=test",
)
}