mirror of
https://github.com/nushell/nushell.git
synced 2025-08-09 09:55:42 +02:00
Parse time type checking for range (#13595)
# Description As part of fixing https://github.com/nushell/nushell/issues/13586, this PR checks the types of the operands when creating a range. Stuff like `0..(glob .)` will be rejected at parse time. Additionally, `0..$x` will be treated as a range and rejected if `x` is not defined, rather than being treated as a string. A separate PR will need to be made to do reject streams at runtime, so that stuff like `0..(open /dev/random)` doesn't hang. Internally, this PR adds a `ParseError::UnsupportedOperationTernary` variant, for when you have a range like `1..2..(glob .)`. # User-Facing Changes Users will now receive an error if any of the operands in the ranges they construct have types that aren't compatible with `Type::Number`. Additionally, if a piece of code looks like a range but some parse error is encountered while parsing it, that piece of code will still be treated as a range and the user will be shown the parse error. This means that a piece of code like `0..$x` will be treated as a range no matter what. Previously, if `x` weren't the expression would've been treated as a string `"0..$x"`. I feel like it makes the language less complicated if we make it less context-sensitive. Here's an example of the error you get: ``` > 0..(glob .) Error: nu::parser::unsupported_operation × range is not supported between int and any. ╭─[entry #1:1:1] 1 │ 0..(glob .) · ─────┬─────┬┬ · │ │╰── any · │ ╰── int · ╰── doesn't support these values ╰──── ``` And as an image:  Note: I made the operands themselves (above, `(glob .)`) be garbage, rather than the `..` operator itself. This doesn't match the behavior of the math operators (if you do `1 + "foo"`, `+` gets highlighted red). This is because with ranges, the range operators aren't `Expression`s themselves, so they can't be turned into garbage. I felt like here, it makes more sense to highlight the individual operand anyway.
This commit is contained in:
@ -2,7 +2,7 @@ use nu_parser::*;
|
||||
use nu_protocol::{
|
||||
ast::{Argument, Expr, Expression, ExternalArgument, PathMember, Range},
|
||||
engine::{Call, Command, EngineState, Stack, StateWorkingSet},
|
||||
ParseError, PipelineData, ShellError, Signature, Span, SyntaxShape, Type,
|
||||
Category, ParseError, PipelineData, ShellError, Signature, Span, SyntaxShape, Type,
|
||||
};
|
||||
use rstest::rstest;
|
||||
|
||||
@ -76,6 +76,35 @@ impl Command for Mut {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ToCustom;
|
||||
|
||||
impl Command for ToCustom {
|
||||
fn name(&self) -> &str {
|
||||
"to-custom"
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Mock converter command."
|
||||
}
|
||||
|
||||
fn signature(&self) -> nu_protocol::Signature {
|
||||
Signature::build(self.name())
|
||||
.input_output_type(Type::Any, Type::Custom("custom".into()))
|
||||
.category(Category::Custom("custom".into()))
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_engine_state: &EngineState,
|
||||
_stack: &mut Stack,
|
||||
_call: &Call,
|
||||
_input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
fn test_int(
|
||||
test_tag: &str, // name of sub-test
|
||||
test: &[u8], // input expression
|
||||
@ -1799,13 +1828,55 @@ mod range {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vars_not_read_as_units() {
|
||||
fn vars_read_as_units() {
|
||||
let engine_state = EngineState::new();
|
||||
let mut working_set = StateWorkingSet::new(&engine_state);
|
||||
|
||||
let _ = parse(&mut working_set, None, b"0..<$day", true);
|
||||
|
||||
assert!(working_set.parse_errors.is_empty());
|
||||
assert!(
|
||||
working_set.parse_errors.len() == 1,
|
||||
"Errors: {:?}",
|
||||
working_set.parse_errors
|
||||
);
|
||||
let err = &working_set.parse_errors[0].to_string();
|
||||
assert!(
|
||||
err.contains("Variable not found"),
|
||||
"Expected variable not found error, got {}",
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case("(to-custom)..")]
|
||||
#[case("..(to-custom)")]
|
||||
#[case("(to-custom)..0")]
|
||||
#[case("..(to-custom)..0")]
|
||||
#[case("(to-custom)..0..")]
|
||||
#[case("(to-custom)..0..1")]
|
||||
#[case("0..(to-custom)")]
|
||||
#[case("0..(to-custom)..")]
|
||||
#[case("0..(to-custom)..1")]
|
||||
#[case("..1..(to-custom)")]
|
||||
#[case("0..1..(to-custom)")]
|
||||
fn type_mismatch_errors(#[case] code: &str) {
|
||||
let engine_state = EngineState::new();
|
||||
let mut working_set = StateWorkingSet::new(&engine_state);
|
||||
working_set.add_decl(Box::new(ToCustom));
|
||||
|
||||
let _ = parse(&mut working_set, None, code.as_bytes(), true);
|
||||
|
||||
assert!(
|
||||
working_set.parse_errors.len() == 1,
|
||||
"Errors: {:?}",
|
||||
working_set.parse_errors
|
||||
);
|
||||
let err = &working_set.parse_errors[0].to_string();
|
||||
assert!(
|
||||
err.contains("range is not supported"),
|
||||
"Expected unsupported operation error, got {}",
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1902,35 +1973,6 @@ mod input_types {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ToCustom;
|
||||
|
||||
impl Command for ToCustom {
|
||||
fn name(&self) -> &str {
|
||||
"to-custom"
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Mock converter command."
|
||||
}
|
||||
|
||||
fn signature(&self) -> nu_protocol::Signature {
|
||||
Signature::build(self.name())
|
||||
.input_output_type(Type::Any, Type::Custom("custom".into()))
|
||||
.category(Category::Custom("custom".into()))
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_engine_state: &EngineState,
|
||||
_stack: &mut Stack,
|
||||
_call: &Call,
|
||||
_input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GroupByCustom;
|
||||
|
||||
|
Reference in New Issue
Block a user