fix(parser): env shorthand false positive (#16337)

More restrictions on env shorthand parsing to avoid false positives.

Fixes #16332
This commit is contained in:
zc he
2025-08-20 23:57:33 +08:00
committed by GitHub
parent 343e5cf191
commit 3877bfb61a
2 changed files with 49 additions and 38 deletions

View File

@@ -116,6 +116,22 @@ pub fn is_math_expression_like(working_set: &mut StateWorkingSet, span: Span) ->
is_range
}
fn is_env_variable_name(bytes: &[u8]) -> bool {
if bytes.is_empty() {
return false;
}
let first = bytes[0];
if !first.is_ascii_alphabetic() && first != b'_' {
return false;
}
bytes
.iter()
.skip(1)
.all(|&b| b.is_ascii_alphanumeric() || b == b'_')
}
fn is_identifier(bytes: &[u8]) -> bool {
bytes.iter().all(|x| is_identifier_byte(*x))
}
@@ -5892,49 +5908,38 @@ pub fn parse_expression(working_set: &mut StateWorkingSet, spans: &[Span]) -> Ex
// Check if there is any environment shorthand
let name = working_set.get_span_contents(spans[pos]);
let split = name.splitn(2, |x| *x == b'=');
let split: Vec<_> = split.collect();
if !name.starts_with(b"^")
&& split.len() == 2
&& !split[0].is_empty()
&& !split[0].ends_with(b"..")
// was range op ..=
{
let point = split[0].len() + 1;
let split: Vec<_> = name.splitn(2, |x| *x == b'=').collect();
if split.len() != 2 || !is_env_variable_name(split[0]) {
break;
}
let starting_error_count = working_set.parse_errors.len();
let point = split[0].len() + 1;
let starting_error_count = working_set.parse_errors.len();
let lhs_span = Span::new(spans[pos].start, spans[pos].start + point - 1);
if !is_identifier(working_set.get_span_contents(lhs_span)) {
break;
}
let lhs = parse_string_strict(working_set, lhs_span);
let rhs = if spans[pos].start + point < spans[pos].end {
let rhs_span = Span::new(spans[pos].start + point, spans[pos].end);
if working_set.get_span_contents(rhs_span).starts_with(b"$") {
parse_dollar_expr(working_set, rhs_span)
} else {
parse_string_strict(working_set, rhs_span)
}
let rhs = if spans[pos].start + point < spans[pos].end {
let rhs_span = Span::new(spans[pos].start + point, spans[pos].end);
if split[1].starts_with(b"$") {
parse_dollar_expr(working_set, rhs_span)
} else {
Expression::new(
working_set,
Expr::String(String::new()),
Span::unknown(),
Type::Nothing,
)
};
if starting_error_count == working_set.parse_errors.len() {
shorthand.push((lhs, rhs));
pos += 1;
} else {
working_set.parse_errors.truncate(starting_error_count);
break;
parse_string_strict(working_set, rhs_span)
}
} else {
Expression::new(
working_set,
Expr::String(String::new()),
Span::unknown(),
Type::Nothing,
)
};
let lhs_span = Span::new(spans[pos].start, spans[pos].start + point - 1);
let lhs = parse_string_strict(working_set, lhs_span);
if starting_error_count == working_set.parse_errors.len() {
shorthand.push((lhs, rhs));
pos += 1;
} else {
working_set.parse_errors.truncate(starting_error_count);
break;
}
}

View File

@@ -1036,3 +1036,9 @@ fn external_argument_with_subexpressions() -> TestResult {
run_test(r#"^echo ")('foo')(""#, ")('foo')(")?;
fail_test(r#"^echo foo( 'bar'"#, "Unexpected end of code")
}
// https://github.com/nushell/nushell/issues/16332
#[test]
fn quote_escape_but_not_env_shorthand() -> TestResult {
run_test(r#""\"=foo""#, "\"=foo")
}