fix(parser): skip eval_const if parsing errors detected to avoid panic (#15364)

Fixes #14972 #15321 #14706

# Description

Early returns `NotAConstant` if parsing errors exist in the
subexpression.

I'm not sure when the span of a block will be None, and whether there're
better ways to handle none block spans, like a more suitable ShellError
type.

# User-Facing Changes

# Tests + Formatting

+1, but possibly not the easiest way to do it.

# After Submitting
This commit is contained in:
zc he
2025-03-26 22:02:26 +08:00
committed by GitHub
parent 55e05be0d8
commit 02fcc485fb
3 changed files with 118 additions and 1 deletions

View File

@ -6,7 +6,7 @@ use nu_protocol::{
};
use rstest::rstest;
use mock::{Alias, AttrEcho, Def, Let, Mut, ToCustom};
use mock::{Alias, AttrEcho, Const, Def, IfMocked, Let, Mut, ToCustom};
fn test_int(
test_tag: &str, // name of sub-test
@ -784,6 +784,38 @@ pub fn parse_attributes_external_alias() {
assert!(parse_error.contains("Encountered error during parse-time evaluation"));
}
#[test]
pub fn parse_if_in_const_expression() {
// https://github.com/nushell/nushell/issues/15321
let engine_state = EngineState::new();
let mut working_set = StateWorkingSet::new(&engine_state);
working_set.add_decl(Box::new(Const));
working_set.add_decl(Box::new(Def));
working_set.add_decl(Box::new(IfMocked));
let source = b"const foo = if t";
let _ = parse(&mut working_set, None, source, false);
assert!(!working_set.parse_errors.is_empty());
let ParseError::MissingPositional(error, _, _) = &working_set.parse_errors[0] else {
panic!("Expected MissingPositional");
};
assert!(error.contains("cond"));
working_set.parse_errors = Vec::new();
let source = b"def a [n= (if ]";
let _ = parse(&mut working_set, None, source, false);
assert!(!working_set.parse_errors.is_empty());
let ParseError::UnexpectedEof(error, _) = &working_set.parse_errors[0] else {
panic!("Expected UnexpectedEof");
};
assert!(error.contains(")"));
}
fn test_external_call(input: &str, tag: &str, f: impl FnOnce(&Expression, &[ExternalArgument])) {
let engine_state = EngineState::new();
let mut working_set = StateWorkingSet::new(&engine_state);
@ -1969,6 +2001,51 @@ mod mock {
engine::Call, Category, IntoPipelineData, PipelineData, ShellError, Type, Value,
};
#[derive(Clone)]
pub struct Const;
impl Command for Const {
fn name(&self) -> &str {
"const"
}
fn description(&self) -> &str {
"Create a parse-time constant."
}
fn signature(&self) -> nu_protocol::Signature {
Signature::build("const")
.input_output_types(vec![(Type::Nothing, Type::Nothing)])
.allow_variants_without_examples(true)
.required("const_name", SyntaxShape::VarWithOptType, "Constant name.")
.required(
"initial_value",
SyntaxShape::Keyword(b"=".to_vec(), Box::new(SyntaxShape::MathExpression)),
"Equals sign followed by constant value.",
)
.category(Category::Core)
}
fn run(
&self,
_engine_state: &EngineState,
_stack: &mut Stack,
_call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
todo!()
}
fn run_const(
&self,
_working_set: &StateWorkingSet,
_call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
Ok(PipelineData::empty())
}
}
#[derive(Clone)]
pub struct Let;
@ -2422,6 +2499,19 @@ mod mock {
) -> Result<PipelineData, ShellError> {
todo!()
}
fn is_const(&self) -> bool {
true
}
fn run_const(
&self,
_working_set: &StateWorkingSet,
_call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
panic!("Should not be called!")
}
}
}