nushell/crates/nu-parser/src/flatten.rs
Devyn Cairns 01891d637d
Make parsing for unknown args in known externals like normal external calls (#13414)
# Description

This corrects the parsing of unknown arguments provided to known
externals to behave exactly like external arguments passed to normal
external calls.

I've done this by adding a `SyntaxShape::ExternalArgument` which
triggers the same parsing rules.

Because I didn't like how the highlighting looked, I modified the
flattener to emit `ExternalArg` flat shapes for arguments that have that
syntax shape and are plain strings/globs. This is the same behavior that
external calls have.

Aside from passing the tests, I've also checked manually that the
completer seems to work adequately. I can confirm that specified
positional arguments get completion according to their specified type
(including custom completions), and then anything remaining gets
filepath style completion, as you'd expect from an external command.

Thanks to @OJarrisonn for originally finding this issue.

# User-Facing Changes

- Unknown args are now parsed according to their specified syntax shape,
rather than `Any`. This may be a breaking change, though I think it's
extremely unlikely in practice.
- The unspecified arguments of known externals are now highlighted /
flattened identically to normal external arguments, which makes it more
clear how they're being interpreted, and should help the completer
function properly.
- Known externals now have an implicit rest arg if not specified named
`args`, with a syntax shape of `ExternalArgument`.

# Tests + Formatting
Tests added for the new behaviour. Some old tests had to be corrected to
match.

- 🟢 `toolkit fmt`
- 🟢 `toolkit clippy`
- 🟢 `toolkit test`
- 🟢 `toolkit test stdlib`

# After Submitting
- [ ] release notes (bugfix, and debatable whether it's a breaking
change)
2024-07-21 01:32:36 -07:00

693 lines
26 KiB
Rust

use nu_protocol::{
ast::{
Argument, Block, Expr, Expression, ExternalArgument, ImportPatternMember, ListItem,
MatchPattern, PathMember, Pattern, Pipeline, PipelineElement, PipelineRedirection,
RecordItem,
},
engine::StateWorkingSet,
DeclId, Span, SyntaxShape, VarId,
};
use std::fmt::{Display, Formatter, Result};
#[derive(Debug, Eq, PartialEq, Ord, Clone, PartialOrd)]
pub enum FlatShape {
And,
Binary,
Block,
Bool,
Closure,
Custom(DeclId),
DateTime,
Directory,
External,
ExternalArg,
ExternalResolved,
Filepath,
Flag,
Float,
Garbage,
GlobInterpolation,
GlobPattern,
Int,
InternalCall(DeclId),
Keyword,
List,
Literal,
MatchPattern,
Nothing,
Operator,
Or,
Pipe,
Range,
RawString,
Record,
Redirection,
Signature,
String,
StringInterpolation,
Table,
Variable(VarId),
VarDecl(VarId),
}
impl FlatShape {
pub fn as_str(&self) -> &str {
match self {
FlatShape::And => "shape_and",
FlatShape::Binary => "shape_binary",
FlatShape::Block => "shape_block",
FlatShape::Bool => "shape_bool",
FlatShape::Closure => "shape_closure",
FlatShape::Custom(_) => "shape_custom",
FlatShape::DateTime => "shape_datetime",
FlatShape::Directory => "shape_directory",
FlatShape::External => "shape_external",
FlatShape::ExternalArg => "shape_externalarg",
FlatShape::ExternalResolved => "shape_external_resolved",
FlatShape::Filepath => "shape_filepath",
FlatShape::Flag => "shape_flag",
FlatShape::Float => "shape_float",
FlatShape::Garbage => "shape_garbage",
FlatShape::GlobInterpolation => "shape_glob_interpolation",
FlatShape::GlobPattern => "shape_globpattern",
FlatShape::Int => "shape_int",
FlatShape::InternalCall(_) => "shape_internalcall",
FlatShape::Keyword => "shape_keyword",
FlatShape::List => "shape_list",
FlatShape::Literal => "shape_literal",
FlatShape::MatchPattern => "shape_match_pattern",
FlatShape::Nothing => "shape_nothing",
FlatShape::Operator => "shape_operator",
FlatShape::Or => "shape_or",
FlatShape::Pipe => "shape_pipe",
FlatShape::Range => "shape_range",
FlatShape::RawString => "shape_raw_string",
FlatShape::Record => "shape_record",
FlatShape::Redirection => "shape_redirection",
FlatShape::Signature => "shape_signature",
FlatShape::String => "shape_string",
FlatShape::StringInterpolation => "shape_string_interpolation",
FlatShape::Table => "shape_table",
FlatShape::Variable(_) => "shape_variable",
FlatShape::VarDecl(_) => "shape_vardecl",
}
}
}
impl Display for FlatShape {
fn fmt(&self, f: &mut Formatter) -> Result {
f.write_str(self.as_str())
}
}
/*
The `_into` functions below (e.g., `flatten_block_into`) take an existing `output` `Vec`
and append more data to it. This is to reduce the number of intermediate `Vec`s.
The non-`into` functions (e.g., `flatten_block`) are part of the crate's public API
and return a new `Vec` instead of modifying an existing one.
*/
fn flatten_block_into(
working_set: &StateWorkingSet,
block: &Block,
output: &mut Vec<(Span, FlatShape)>,
) {
for pipeline in &block.pipelines {
flatten_pipeline_into(working_set, pipeline, output);
}
}
fn flatten_pipeline_into(
working_set: &StateWorkingSet,
pipeline: &Pipeline,
output: &mut Vec<(Span, FlatShape)>,
) {
for expr in &pipeline.elements {
flatten_pipeline_element_into(working_set, expr, output)
}
}
fn flatten_pipeline_element_into(
working_set: &StateWorkingSet,
pipeline_element: &PipelineElement,
output: &mut Vec<(Span, FlatShape)>,
) {
if let Some(span) = pipeline_element.pipe {
output.push((span, FlatShape::Pipe));
}
flatten_expression_into(working_set, &pipeline_element.expr, output);
if let Some(redirection) = pipeline_element.redirection.as_ref() {
match redirection {
PipelineRedirection::Single { target, .. } => {
output.push((target.span(), FlatShape::Redirection));
if let Some(expr) = target.expr() {
flatten_expression_into(working_set, expr, output);
}
}
PipelineRedirection::Separate { out, err } => {
let (out, err) = if out.span() <= err.span() {
(out, err)
} else {
(err, out)
};
output.push((out.span(), FlatShape::Redirection));
if let Some(expr) = out.expr() {
flatten_expression_into(working_set, expr, output);
}
output.push((err.span(), FlatShape::Redirection));
if let Some(expr) = err.expr() {
flatten_expression_into(working_set, expr, output);
}
}
}
}
}
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(
working_set: &StateWorkingSet,
expr: &Expression,
output: &mut Vec<(Span, FlatShape)>,
) {
if let Some(custom_completion) = &expr.custom_completion {
output.push((expr.span, FlatShape::Custom(*custom_completion)));
return;
}
match &expr.expr {
Expr::BinaryOp(lhs, op, rhs) => {
flatten_expression_into(working_set, lhs, output);
flatten_expression_into(working_set, op, output);
flatten_expression_into(working_set, rhs, output);
}
Expr::UnaryNot(not) => {
output.push((
Span::new(expr.span.start, expr.span.start + 3),
FlatShape::Operator,
));
flatten_expression_into(working_set, not, output);
}
Expr::Collect(_, expr) => {
flatten_expression_into(working_set, expr, output);
}
Expr::Closure(block_id) => {
let outer_span = expr.span;
let block = working_set.get_block(*block_id);
let flattened = flatten_block(working_set, block);
if let Some(first) = flattened.first() {
if first.0.start > outer_span.start {
output.push((
Span::new(outer_span.start, first.0.start),
FlatShape::Closure,
));
}
}
let last = if let Some(last) = flattened.last() {
if last.0.end < outer_span.end {
Some((Span::new(last.0.end, outer_span.end), FlatShape::Closure))
} else {
None
}
} else {
None
};
output.extend(flattened);
if let Some(last) = last {
output.push(last);
}
}
Expr::Block(block_id) | Expr::RowCondition(block_id) | Expr::Subexpression(block_id) => {
let outer_span = expr.span;
let flattened = flatten_block(working_set, working_set.get_block(*block_id));
if let Some(first) = flattened.first() {
if first.0.start > outer_span.start {
output.push((Span::new(outer_span.start, first.0.start), FlatShape::Block));
}
}
let last = if let Some(last) = flattened.last() {
if last.0.end < outer_span.end {
Some((Span::new(last.0.end, outer_span.end), FlatShape::Block))
} else {
None
}
} else {
None
};
output.extend(flattened);
if let Some(last) = last {
output.push(last);
}
}
Expr::Call(call) => {
let decl = working_set.get_decl(call.decl_id);
if call.head.end != 0 {
// Make sure we don't push synthetic calls
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();
for arg in &call.arguments {
match arg {
Argument::Positional(positional) => {
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) => {
if named.0.span.end != 0 {
// Ignore synthetic flags
output.push((named.0.span, FlatShape::Flag));
}
if let Some(expr) = &named.2 {
flatten_expression_into(working_set, expr, output);
}
}
Argument::Spread(expr) => {
output.push((
Span::new(expr.span.start - 3, expr.span.start),
FlatShape::Operator,
));
flatten_expression_into(working_set, expr, output);
}
}
}
// sort these since flags and positional args can be intermixed
output[arg_start..].sort();
}
Expr::ExternalCall(head, args) => {
if let Expr::String(..) | Expr::GlobPattern(..) = &head.expr {
output.push((head.span, FlatShape::External));
} else {
flatten_expression_into(working_set, head, output);
}
for arg in args.as_ref() {
match arg {
ExternalArgument::Regular(expr) => {
if let Expr::String(..) | Expr::GlobPattern(..) = &expr.expr {
output.push((expr.span, FlatShape::ExternalArg));
} else {
flatten_expression_into(working_set, expr, output);
}
}
ExternalArgument::Spread(expr) => {
output.push((
Span::new(expr.span.start - 3, expr.span.start),
FlatShape::Operator,
));
flatten_expression_into(working_set, expr, output);
}
}
}
}
Expr::Garbage => output.push((expr.span, FlatShape::Garbage)),
Expr::Nothing => output.push((expr.span, FlatShape::Nothing)),
Expr::DateTime(_) => output.push((expr.span, FlatShape::DateTime)),
Expr::Binary(_) => output.push((expr.span, FlatShape::Binary)),
Expr::Int(_) => output.push((expr.span, FlatShape::Int)),
Expr::Float(_) => output.push((expr.span, FlatShape::Float)),
Expr::MatchBlock(matches) => {
for (pattern, expr) in matches {
flatten_pattern_into(pattern, output);
flatten_expression_into(working_set, expr, output);
}
}
Expr::ValueWithUnit(value) => {
flatten_expression_into(working_set, &value.expr, output);
output.push((value.unit.span, FlatShape::String));
}
Expr::CellPath(cell_path) => {
output.extend(cell_path.members.iter().map(|member| match *member {
PathMember::String { span, .. } => (span, FlatShape::String),
PathMember::Int { span, .. } => (span, FlatShape::Int),
}));
}
Expr::FullCellPath(cell_path) => {
flatten_expression_into(working_set, &cell_path.head, output);
output.extend(cell_path.tail.iter().map(|member| match *member {
PathMember::String { span, .. } => (span, FlatShape::String),
PathMember::Int { span, .. } => (span, FlatShape::Int),
}));
}
Expr::ImportPattern(import_pattern) => {
output.push((import_pattern.head.span, FlatShape::String));
for member in &import_pattern.members {
match member {
ImportPatternMember::Glob { span } => output.push((*span, FlatShape::String)),
ImportPatternMember::Name { span, .. } => {
output.push((*span, FlatShape::String))
}
ImportPatternMember::List { names } => {
output.extend(names.iter().map(|&(_, span)| (span, FlatShape::String)))
}
}
}
}
Expr::Overlay(_) => output.push((expr.span, FlatShape::String)),
Expr::Range(range) => {
if let Some(f) = &range.from {
flatten_expression_into(working_set, f, output);
}
if let Some(s) = &range.next {
output.push((range.operator.next_op_span, FlatShape::Operator));
flatten_expression_into(working_set, s, output);
}
output.push((range.operator.span, FlatShape::Operator));
if let Some(t) = &range.to {
flatten_expression_into(working_set, t, output);
}
}
Expr::Bool(_) => output.push((expr.span, FlatShape::Bool)),
Expr::Filepath(_, _) => output.push((expr.span, FlatShape::Filepath)),
Expr::Directory(_, _) => output.push((expr.span, FlatShape::Directory)),
Expr::GlobPattern(_, _) => output.push((expr.span, FlatShape::GlobPattern)),
Expr::List(list) => {
let outer_span = expr.span;
let mut last_end = outer_span.start;
for item in list {
match item {
ListItem::Item(expr) => {
let flattened = flatten_expression(working_set, expr);
if let Some(first) = flattened.first() {
if first.0.start > last_end {
output.push((Span::new(last_end, first.0.start), FlatShape::List));
}
}
if let Some(last) = flattened.last() {
last_end = last.0.end;
}
output.extend(flattened);
}
ListItem::Spread(op_span, expr) => {
if op_span.start > last_end {
output.push((Span::new(last_end, op_span.start), FlatShape::List));
}
output.push((*op_span, FlatShape::Operator));
last_end = op_span.end;
let flattened_inner = flatten_expression(working_set, expr);
if let Some(first) = flattened_inner.first() {
if first.0.start > last_end {
output.push((Span::new(last_end, first.0.start), FlatShape::List));
}
}
if let Some(last) = flattened_inner.last() {
last_end = last.0.end;
}
output.extend(flattened_inner);
}
}
}
if last_end < outer_span.end {
output.push((Span::new(last_end, outer_span.end), FlatShape::List));
}
}
Expr::StringInterpolation(exprs) => {
let mut flattened = vec![];
for expr in exprs {
flatten_expression_into(working_set, expr, &mut flattened);
}
if let Some(first) = flattened.first() {
if first.0.start != expr.span.start {
// If we aren't a bare word interpolation, also highlight the outer quotes
output.push((
Span::new(expr.span.start, expr.span.start + 2),
FlatShape::StringInterpolation,
));
flattened.push((
Span::new(expr.span.end - 1, expr.span.end),
FlatShape::StringInterpolation,
));
}
}
output.extend(flattened);
}
Expr::GlobInterpolation(exprs, quoted) => {
let mut flattened = vec![];
for expr in exprs {
flatten_expression_into(working_set, expr, &mut flattened);
}
if *quoted {
// If we aren't a bare word interpolation, also highlight the outer quotes
output.push((
Span::new(expr.span.start, expr.span.start + 2),
FlatShape::GlobInterpolation,
));
flattened.push((
Span::new(expr.span.end - 1, expr.span.end),
FlatShape::GlobInterpolation,
));
}
output.extend(flattened);
}
Expr::Record(list) => {
let outer_span = expr.span;
let mut last_end = outer_span.start;
for l in list {
match l {
RecordItem::Pair(key, val) => {
let flattened_lhs = flatten_expression(working_set, key);
let flattened_rhs = flatten_expression(working_set, val);
if let Some(first) = flattened_lhs.first() {
if first.0.start > last_end {
output
.push((Span::new(last_end, first.0.start), FlatShape::Record));
}
}
if let Some(last) = flattened_lhs.last() {
last_end = last.0.end;
}
output.extend(flattened_lhs);
if let Some(first) = flattened_rhs.first() {
if first.0.start > last_end {
output
.push((Span::new(last_end, first.0.start), FlatShape::Record));
}
}
if let Some(last) = flattened_rhs.last() {
last_end = last.0.end;
}
output.extend(flattened_rhs);
}
RecordItem::Spread(op_span, record) => {
if op_span.start > last_end {
output.push((Span::new(last_end, op_span.start), FlatShape::Record));
}
output.push((*op_span, FlatShape::Operator));
last_end = op_span.end;
let flattened = flatten_expression(working_set, record);
if let Some(first) = flattened.first() {
if first.0.start > last_end {
output
.push((Span::new(last_end, first.0.start), FlatShape::Record));
}
}
if let Some(last) = flattened.last() {
last_end = last.0.end;
}
output.extend(flattened);
}
}
}
if last_end < outer_span.end {
output.push((Span::new(last_end, outer_span.end), FlatShape::Record));
}
}
Expr::Keyword(kw) => {
output.push((kw.span, FlatShape::Keyword));
flatten_expression_into(working_set, &kw.expr, output);
}
Expr::Operator(_) => output.push((expr.span, FlatShape::Operator)),
Expr::Signature(_) => output.push((expr.span, FlatShape::Signature)),
Expr::String(_) => output.push((expr.span, FlatShape::String)),
Expr::RawString(_) => output.push((expr.span, FlatShape::RawString)),
Expr::Table(table) => {
let outer_span = expr.span;
let mut last_end = outer_span.start;
for col in table.columns.as_ref() {
let flattened = flatten_expression(working_set, col);
if let Some(first) = flattened.first() {
if first.0.start > last_end {
output.push((Span::new(last_end, first.0.start), FlatShape::Table));
}
}
if let Some(last) = flattened.last() {
last_end = last.0.end;
}
output.extend(flattened);
}
for row in table.rows.as_ref() {
for expr in row.as_ref() {
let flattened = flatten_expression(working_set, expr);
if let Some(first) = flattened.first() {
if first.0.start > last_end {
output.push((Span::new(last_end, first.0.start), FlatShape::Table));
}
}
if let Some(last) = flattened.last() {
last_end = last.0.end;
}
output.extend(flattened);
}
}
if last_end < outer_span.end {
output.push((Span::new(last_end, outer_span.end), FlatShape::Table));
}
}
Expr::Var(var_id) => output.push((expr.span, FlatShape::Variable(*var_id))),
Expr::VarDecl(var_id) => output.push((expr.span, FlatShape::VarDecl(*var_id))),
}
}
fn flatten_pattern_into(match_pattern: &MatchPattern, output: &mut Vec<(Span, FlatShape)>) {
match &match_pattern.pattern {
Pattern::Garbage => output.push((match_pattern.span, FlatShape::Garbage)),
Pattern::IgnoreValue => output.push((match_pattern.span, FlatShape::Nothing)),
Pattern::IgnoreRest => output.push((match_pattern.span, FlatShape::Nothing)),
Pattern::List(items) => {
if let Some(first) = items.first() {
if let Some(last) = items.last() {
output.push((
Span::new(match_pattern.span.start, first.span.start),
FlatShape::MatchPattern,
));
for item in items {
flatten_pattern_into(item, output);
}
output.push((
Span::new(last.span.end, match_pattern.span.end),
FlatShape::MatchPattern,
))
}
} else {
output.push((match_pattern.span, FlatShape::MatchPattern));
}
}
Pattern::Record(items) => {
if let Some(first) = items.first() {
if let Some(last) = items.last() {
output.push((
Span::new(match_pattern.span.start, first.1.span.start),
FlatShape::MatchPattern,
));
for (_, pattern) in items {
flatten_pattern_into(pattern, output);
}
output.push((
Span::new(last.1.span.end, match_pattern.span.end),
FlatShape::MatchPattern,
))
}
} else {
output.push((match_pattern.span, FlatShape::MatchPattern));
}
}
Pattern::Value(_) => output.push((match_pattern.span, FlatShape::MatchPattern)),
Pattern::Variable(var_id) => output.push((match_pattern.span, FlatShape::VarDecl(*var_id))),
Pattern::Rest(var_id) => output.push((match_pattern.span, FlatShape::VarDecl(*var_id))),
Pattern::Or(patterns) => {
for pattern in patterns {
flatten_pattern_into(pattern, output);
}
}
}
}
pub fn flatten_block(working_set: &StateWorkingSet, block: &Block) -> Vec<(Span, FlatShape)> {
let mut output = Vec::new();
flatten_block_into(working_set, block, &mut output);
output
}
pub fn flatten_pipeline(
working_set: &StateWorkingSet,
pipeline: &Pipeline,
) -> Vec<(Span, FlatShape)> {
let mut output = Vec::new();
flatten_pipeline_into(working_set, pipeline, &mut output);
output
}
pub fn flatten_pipeline_element(
working_set: &StateWorkingSet,
pipeline_element: &PipelineElement,
) -> Vec<(Span, FlatShape)> {
let mut output = Vec::new();
flatten_pipeline_element_into(working_set, pipeline_element, &mut output);
output
}
pub fn flatten_expression(
working_set: &StateWorkingSet,
expr: &Expression,
) -> Vec<(Span, FlatShape)> {
let mut output = Vec::new();
flatten_expression_into(working_set, expr, &mut output);
output
}