Allow foo=bar global variable assignments

override fixes
This commit is contained in:
Johannes Altmanninger
2024-11-21 11:02:34 +01:00
parent 0199583435
commit 45a2017580
11 changed files with 268 additions and 144 deletions

View File

@@ -14,7 +14,7 @@
use crate::parse_constants::{
token_type_user_presentable_description, ParseError, ParseErrorCode, ParseErrorList,
ParseKeyword, ParseTokenType, ParseTreeFlags, SourceRange, StatementDecoration,
ERROR_BAD_COMMAND_ASSIGN_ERR_MSG, INVALID_PIPELINE_CMD_ERR_MSG, SOURCE_OFFSET_INVALID,
INVALID_PIPELINE_CMD_ERR_MSG, SOURCE_OFFSET_INVALID,
};
use crate::parse_tree::ParseToken;
#[cfg(test)]
@@ -55,7 +55,12 @@ pub struct MissingEndError {
token: ParseToken,
}
pub type VisitResult = ControlFlow<MissingEndError>;
pub enum NonLocalError {
MissingEndError(MissingEndError),
MissingStatementError,
}
pub type VisitResult = ControlFlow<NonLocalError>;
trait NodeVisitorMut {
/// will_visit (did_visit) is called before (after) a node's fields are visited.
@@ -85,6 +90,7 @@ fn visit_decorated_statement_decorator(
fn visit_then(&mut self, _node: &mut Option<KeywordThen>);
fn visit_time(&mut self, _node: &mut Option<KeywordTime>);
fn visit_token_background(&mut self, _node: &mut Option<TokenBackground>);
fn visit_optional_statement(&mut self, _node: &mut Option<Statement>);
}
trait AcceptorMut {
@@ -185,6 +191,9 @@ fn as_argument_or_redirection_list(&self) -> Option<&ArgumentOrRedirectionList>
fn as_command_token(&self) -> Option<&CommandToken> {
None
}
fn as_variable_statement(&self) -> Option<&VariableStatement> {
None
}
fn as_statement(&self) -> Option<&Statement> {
None
}
@@ -309,6 +318,9 @@ fn as_mut_argument_or_redirection_list(&mut self) -> Option<&mut ArgumentOrRedir
fn as_mut_command_token(&mut self) -> Option<&mut CommandToken> {
None
}
fn as_mut_variable_statement(&mut self) -> Option<&mut VariableStatement> {
None
}
fn as_mut_statement(&mut self) -> Option<&mut Statement> {
None
}
@@ -988,6 +1000,9 @@ macro_rules! visit_optional_field_mut {
(TokenBackground, $field:expr, $visitor:ident) => {
$visitor.visit_token_background(&mut $field);
};
(Statement, $field:expr, $visitor:ident) => {
$visitor.visit_optional_statement(&mut $field);
};
}
macro_rules! visit_result {
@@ -1227,6 +1242,31 @@ pub fn is_left_brace(&self) -> bool {
}
}
#[derive(Default, Debug)]
pub struct VariableStatement {
parent: Option<*const dyn Node>,
/// A (possibly empty) list of variable assignments.
pub variables: VariableAssignmentList,
/// The statement.
pub statement: Option<Statement>,
}
implement_node!(VariableStatement, branch, variable_statement);
implement_acceptor_for_branch!(
VariableStatement,
(variables: (VariableAssignmentList)),
(statement: (Option<Statement>)),
);
impl ConcreteNode for VariableStatement {
fn as_variable_statement(&self) -> Option<&VariableStatement> {
Some(self)
}
}
impl ConcreteNodeMut for VariableStatement {
fn as_mut_variable_statement(&mut self) -> Option<&mut VariableStatement> {
Some(self)
}
}
/// A statement is a normal command, or an if / while / etc
#[derive(Default, Debug)]
pub struct Statement {
@@ -1246,6 +1286,16 @@ fn as_mut_statement(&mut self) -> Option<&mut Statement> {
}
}
impl CheckParse for Statement {
fn can_be_parsed(pop: &mut Populator<'_>) -> bool {
let next = pop.peek_token(0);
matches!(
next.typ,
ParseTokenType::terminate | ParseTokenType::left_brace
) || (next.typ == ParseTokenType::string && !next.is_variable_assignment)
}
}
/// A job is a non-empty list of statements, separated by pipes. (Non-empty is useful for cases
/// like if statements, where we require a command).
#[derive(Default, Debug)]
@@ -1253,10 +1303,7 @@ pub struct JobPipeline {
parent: Option<*const dyn Node>,
/// Maybe the time keyword.
pub time: Option<KeywordTime>,
/// A (possibly empty) list of variable assignments.
pub variables: VariableAssignmentList,
/// The statement.
pub statement: Statement,
pub statement2: VariableStatement,
/// Piped remainder.
pub continuation: JobContinuationList,
/// Maybe backgrounded.
@@ -1266,8 +1313,7 @@ pub struct JobPipeline {
implement_acceptor_for_branch!(
JobPipeline,
(time: (Option<KeywordTime>)),
(variables: (VariableAssignmentList)),
(statement: (Statement)),
(statement2: (VariableStatement)),
(continuation: (JobContinuationList)),
(bg: (Option<TokenBackground>)),
);
@@ -1281,6 +1327,11 @@ fn as_mut_job_pipeline(&mut self) -> Option<&mut JobPipeline> {
Some(self)
}
}
impl JobPipeline {
pub fn statement3(&self) -> Option<&Statement> {
self.statement2.statement.as_ref()
}
}
/// A job_conjunction is a job followed by a && or || continuations.
#[derive(Default, Debug)]
@@ -1759,16 +1810,14 @@ pub struct NotStatement {
/// Keyword, either not or exclam.
pub kw: KeywordNot,
pub time: Option<KeywordTime>,
pub variables: VariableAssignmentList,
pub contents: Statement,
pub negated_statement: VariableStatement,
}
implement_node!(NotStatement, branch, not_statement);
implement_acceptor_for_branch!(
NotStatement,
(kw: (KeywordNot)),
(time: (Option<KeywordTime>)),
(variables: (VariableAssignmentList)),
(contents: (Statement)),
(negated_statement: (VariableStatement)),
);
impl ConcreteNode for NotStatement {
fn as_not_statement(&self) -> Option<&NotStatement> {
@@ -1786,16 +1835,14 @@ pub struct JobContinuation {
parent: Option<*const dyn Node>,
pub pipe: TokenPipe,
pub newlines: MaybeNewlines,
pub variables: VariableAssignmentList,
pub statement: Statement,
pub statement2: VariableStatement,
}
implement_node!(JobContinuation, branch, job_continuation);
implement_acceptor_for_branch!(
JobContinuation,
(pipe: (TokenPipe)),
(newlines: (MaybeNewlines)),
(variables: (VariableAssignmentList)),
(statement: (Statement)),
(statement2: (VariableStatement)),
);
impl ConcreteNode for JobContinuation {
fn as_job_continuation(&self) -> Option<&JobContinuation> {
@@ -1812,6 +1859,11 @@ fn can_be_parsed(pop: &mut Populator<'_>) -> bool {
pop.peek_type(0) == ParseTokenType::pipe
}
}
impl JobContinuation {
pub fn statement(&self) -> Option<&Statement> {
self.statement2.statement.as_ref()
}
}
define_list_node!(JobContinuationList, job_continuation_list, JobContinuation);
impl ConcreteNode for JobContinuationList {
@@ -2010,20 +2062,18 @@ fn as_mut_variable_assignment(&mut self) -> Option<&mut VariableAssignment> {
impl CheckParse for VariableAssignment {
fn can_be_parsed(pop: &mut Populator<'_>) -> bool {
// Do we have a variable assignment at all?
if !pop.peek_token(0).may_be_variable_assignment {
return false;
}
// What is the token after it?
match pop.peek_type(1) {
// We have `a= cmd` and should treat it as a variable assignment.
ParseTokenType::string | ParseTokenType::left_brace => true,
// We have `a=` which is OK if we are allowing incomplete, an error otherwise.
ParseTokenType::terminate => pop.allow_incomplete(),
// We have e.g. `a= >` which is an error.
// Note that we do not produce an error here. Instead we return false
// so this the token will be seen by allocate_populate_statement_contents.
_ => false,
}
pop.peek_token(0).is_variable_assignment
// // What is the token after it?
// match pop.peek_type(1) {
// // We have `a= cmd` and should treat it as a variable assignment.
// ParseTokenType::string | ParseTokenType::left_brace => true,
// // We have `a=` which is OK if we are allowing incomplete, an error otherwise.
// ParseTokenType::terminate => pop.allow_incomplete(),
// // We have e.g. `a= >` which is an error.
// // Note that we do not produce an error here. Instead we return false
// // so this the token will be seen by allocate_populate_statement_contents.
// _ => false,
// }
}
}
@@ -2578,6 +2628,7 @@ pub fn ast_type_to_string(t: Type) -> &'static wstr {
Type::argument_or_redirection => L!("argument_or_redirection"),
Type::argument_or_redirection_list => L!("argument_or_redirection_list"),
Type::command_token => L!("command_token"),
Type::variable_statement => L!("variable_statement"),
Type::statement => L!("statement"),
Type::job_pipeline => L!("job_pipeline"),
Type::job_conjunction => L!("job_conjunction"),
@@ -2919,7 +2970,7 @@ fn advance_1(&mut self) -> ParseToken {
result.has_dash_prefix = text.starts_with('-');
result.is_help_argument = [L!("-h"), L!("--help")].contains(&text);
result.is_newline = result.typ == ParseTokenType::end && text == "\n";
result.may_be_variable_assignment = variable_assignment_equals_pos(text).is_some();
result.is_variable_assignment = variable_assignment_equals_pos(text).is_some();
result.tok_error = token.error;
assert!(token.offset() < SOURCE_OFFSET_INVALID);
@@ -3029,6 +3080,9 @@ struct Populator<'a> {
/// Stream of tokens which we consume.
tokens: TokenStream<'a>,
/// Whether this statement has a prefixed variable override.
parsing_variable_statement: Option<bool>,
/** The type which we are attempting to parse, typically job_list but may be
freestanding_argument_list. */
top_type: Type,
@@ -3080,9 +3134,42 @@ fn visit_mut(&mut self, node: &mut dyn NodeMut) -> VisitResult {
Category::leaf => {}
// Visit branch nodes by just calling accept() to visit their fields.
Category::branch => {
self.parsing_variable_statement = Some(
node.as_variable_statement().is_some()
&& self.peek_token(0).is_variable_assignment,
);
// This field is a direct embedding of an AST value.
node.accept_mut(self, false);
return VisitResult::Continue(());
let Some(variable_statement) = node.as_mut_variable_statement() else {
return VisitResult::Continue(());
};
if self.peek_token(0).typ == ParseTokenType::terminate {
return VisitResult::Continue(());
}
if variable_statement.statement.as_ref().is_some_and(|stmt|
// TODO(posix_mode) # Un-clone
// This may happen if we just have a 'time' prefix.
// Construct a decorated statement, which will be unsourced.
stmt
.contents
.as_decorated_statement()
.is_some_and(|ds| ds.try_source_range().is_none()))
{
variable_statement.statement = None;
}
if variable_statement.statement.is_some()
|| !variable_statement.variables.is_empty()
{
return VisitResult::Continue(());
}
parse_error!(
self,
self.peek_token(0),
ParseErrorCode::generic,
"asdf Expected a command but found %ls",
self.peek_token(0).user_presentable_description()
);
return VisitResult::Break(NonLocalError::MissingStatementError);
}
Category::list => {
// This field is an embedding of an array of (pointers to) ContentsNode.
@@ -3150,6 +3237,9 @@ fn did_visit_fields_of<'a>(&'a mut self, node: &'a dyn NodeMut, flow: VisitResul
let VisitResult::Break(error) = flow else {
return;
};
let NonLocalError::MissingEndError(error) = error else {
return;
};
let token = &error.token;
// To-do: maybe extend this to other tokenizer errors?
@@ -3304,6 +3394,14 @@ fn visit_then(&mut self, node: &mut Option<KeywordThen>) {
fn visit_token_background(&mut self, node: &mut Option<TokenBackground>) {
*node = self.try_parse::<TokenBackground>().map(|b| *b);
}
fn visit_optional_statement(&mut self, node: &mut Option<Statement>) {
if self.parsing_variable_statement.take().unwrap()
&& self.peek_token(0).typ == ParseTokenType::terminate
{
return;
}
*node = self.try_parse::<Statement>().map(|b| *b);
}
}
/// Helper to describe a list of keywords.
@@ -3350,6 +3448,7 @@ fn new(
semis: vec![],
errors: vec![],
tokens: TokenStream::new(src, flags, top_type == Type::freestanding_argument_list),
parsing_variable_statement: None,
top_type,
unwinding: false,
any_error: false,
@@ -3858,25 +3957,26 @@ fn new_decorated_statement(slf: &mut Populator<'_>) -> StatementVariant {
self.peek_token(0).user_presentable_description()
);
return got_error(self);
} else if self.peek_token(0).may_be_variable_assignment {
} else if self.peek_token(0).is_variable_assignment {
// Here we have a variable assignment which we chose to not parse as a variable
// assignment because there was no string after it.
// Ensure we consume the token, so we don't get back here again at the same place.
let token = &self.consume_any_token();
let text = &self.tokens.src
[token.source_start()..token.source_start() + token.source_length()];
let equals_pos = variable_assignment_equals_pos(text).unwrap();
let variable = &text[..equals_pos];
let value = &text[equals_pos + 1..];
parse_error!(
self,
token,
ParseErrorCode::bare_variable_assignment,
ERROR_BAD_COMMAND_ASSIGN_ERR_MSG,
variable,
value
);
return got_error(self);
// let token = &self.consume_any_token();
// let text = &self.tokens.src
// [token.source_start()..token.source_start() + token.source_length()];
// let equals_pos = variable_assignment_equals_pos(text).unwrap();
// let variable = &text[..equals_pos];
// let value = &text[equals_pos + 1..];
// parse_error!(
// self,
// token,
// ParseErrorCode::bare_variable_assignment,
// ERROR_BAD_COMMAND_ASSIGN_ERR_MSG,
// variable,
// value
// );
// return got_error(self);
return new_decorated_statement(self);
}
// In some cases a block starter is a decorated statement instead, mostly if invoked with "--help".
@@ -4035,7 +4135,7 @@ fn visit_variable_assignment(&mut self, varas: &mut VariableAssignment) {
varas.range = None;
return;
}
if !self.peek_token(0).may_be_variable_assignment {
if !self.peek_token(0).is_variable_assignment {
internal_error!(
self,
visit_variable_assignment,
@@ -4116,10 +4216,10 @@ fn visit_keyword(&mut self, keyword: &mut dyn Keyword) -> VisitResult {
// Special error reporting for keyword_t<kw_end>.
let allowed_keywords = keyword.allowed_keywords();
if allowed_keywords.contains(&ParseKeyword::kw_end) {
return VisitResult::Break(MissingEndError {
return VisitResult::Break(NonLocalError::MissingEndError(MissingEndError {
allowed_keywords,
token: *self.peek_token(0),
});
}));
} else {
parse_error!(
self,
@@ -4334,6 +4434,7 @@ pub enum Type {
argument_or_redirection,
argument_or_redirection_list,
command_token,
variable_statement,
statement,
job_pipeline,
job_conjunction,

View File

@@ -349,18 +349,20 @@ fn gap_text_flags_before_node(&self, node: &dyn Node) -> GapFlags {
let p = p.parent().unwrap();
assert_eq!(p.typ(), Type::statement);
let p = p.parent().unwrap();
if let Some(job) = p.as_job_pipeline() {
if !job.variables.is_empty() {
result.allow_escaped_newlines = true;
}
#[allow(clippy::manual_map)]
if (if let Some(job) = p.as_job_pipeline() {
Some(&job.statement2)
} else if let Some(job_cnt) = p.as_job_continuation() {
if !job_cnt.variables.is_empty() {
result.allow_escaped_newlines = true;
}
Some(&job_cnt.statement2)
} else if let Some(not_stmt) = p.as_not_statement() {
if !not_stmt.variables.is_empty() {
result.allow_escaped_newlines = true;
}
Some(&not_stmt.negated_statement)
} else {
None
})
.is_some_and(|stmt| !stmt.variables.is_empty())
{
result.allow_escaped_newlines = true;
result.allow_escaped_newlines = true;
}
}
_ => (),

View File

@@ -283,7 +283,7 @@ fn autosuggest_parse_command(
// Find the first statement.
let jc = ast.top().as_job_list().unwrap().get(0)?;
let first_statement = jc.job.statement.contents.as_decorated_statement()?;
let first_statement = jc.job.statement3()?.contents.as_decorated_statement()?;
if let Some(expanded_command) = statement_get_expanded_command(buff, first_statement, ctx) {
let mut arg = WString::new();

View File

@@ -138,12 +138,11 @@ pub enum ParseErrorCode {
tokenizer_unterminated_escape,
tokenizer_other,
unbalancing_end, // end outside of block
unbalancing_else, // else outside of if
unbalancing_case, // case outside of switch
unbalancing_brace, // } outside of {
bare_variable_assignment, // a=b without command
andor_in_pipeline, // "and" or "or" after a pipe
unbalancing_end, // end outside of block
unbalancing_else, // else outside of if
unbalancing_case, // case outside of switch
unbalancing_brace, // } outside of {
andor_in_pipeline, // "and" or "or" after a pipe
}
// The location of a pipeline.

View File

@@ -450,11 +450,14 @@ fn infinite_recursive_statement_in_job_list<'b>(
};
// Check main statement.
let infinite_recursive_statement = statement_recurses(&jc.job.statement)
let infinite_recursive_statement = statement_recurses(jc.job.statement3()?)
// Check piped remainder.
.or_else(|| {
for c in &job.continuation {
let s = statement_recurses(&c.statement);
let Some(stmt) = c.statement() else {
continue;
};
let s = statement_recurses(stmt);
if s.is_some() {
return s;
}
@@ -564,9 +567,12 @@ fn job_is_simple_block(&self, job: &ast::JobPipeline) -> bool {
let no_redirs =
|list: &ast::ArgumentOrRedirectionList| !list.iter().any(|val| val.is_redirection());
let Some(statement) = job.statement3() else {
return true;
};
// Check if we're a block statement with redirections. We do it this obnoxious way to preserve
// type safety (in case we add more specific statement types).
match &job.statement.contents {
match &statement.contents {
StatementVariant::BlockStatement(stmt) => no_redirs(&stmt.args_or_redirs),
StatementVariant::BraceStatement(stmt) => no_redirs(&stmt.args_or_redirs),
StatementVariant::SwitchStatement(stmt) => no_redirs(&stmt.args_or_redirs),
@@ -608,12 +614,17 @@ fn apply_variable_assignments(
ctx: &OperationContext<'_>,
mut proc: Option<&mut Process>,
variable_assignment_list: &ast::VariableAssignmentList,
block: &mut Option<BlockId>,
block: Option<&mut Option<BlockId>>,
) -> EndExecutionReason {
if variable_assignment_list.is_empty() {
return EndExecutionReason::ok;
}
*block = Some(ctx.parser().push_block(Block::variable_assignment_block()));
// FIXME EnvMode::USER
let mut flags = EnvMode::default();
if let Some(block) = block {
*block = Some(ctx.parser().push_block(Block::variable_assignment_block()));
flags = EnvMode::LOCAL | EnvMode::EXPORT;
}
for variable_assignment in variable_assignment_list {
let source = self.node_source(&**variable_assignment);
let equals_pos = variable_assignment_equals_pos(source).unwrap();
@@ -653,8 +664,7 @@ fn apply_variable_assignments(
vals.clone(),
));
}
ctx.parser()
.set_var_and_fire(variable_name, EnvMode::LOCAL | EnvMode::EXPORT, vals);
ctx.parser().set_var_and_fire(variable_name, flags, vals);
}
EndExecutionReason::ok
}
@@ -665,17 +675,20 @@ fn populate_job_process(
ctx: &OperationContext<'_>,
job: &mut Job,
proc: &mut Process,
statement: &ast::Statement,
variable_assignments: &ast::VariableAssignmentList,
variable_statement: &ast::VariableStatement,
) -> EndExecutionReason {
// Get the "specific statement" which is boolean / block / if / switch / decorated.
let specific_statement = &statement.contents;
let mut block = None;
let result =
self.apply_variable_assignments(ctx, Some(proc), variable_assignments, &mut block);
let mut block = variable_statement.statement.is_some().then_some(None);
let result = self.apply_variable_assignments(
ctx,
Some(proc),
&variable_statement.variables,
block.as_mut(),
);
let Some(statement) = variable_statement.statement.as_ref() else {
return result;
};
let _scope = ScopeGuard::new((), |()| {
if let Some(block) = block {
if let Some(Some(block)) = block {
ctx.parser().pop_block(block);
}
});
@@ -683,6 +696,8 @@ fn populate_job_process(
return result;
}
// Get the "specific statement" which is boolean / block / if / switch / decorated.
let specific_statement = &statement.contents;
match &specific_statement {
StatementVariant::NotStatement(not_statement) => {
self.populate_not_process(ctx, job, proc, not_statement)
@@ -711,13 +726,7 @@ fn populate_not_process(
let mut flags = job.mut_flags();
flags.negate = !flags.negate;
}
self.populate_job_process(
ctx,
job,
proc,
&not_statement.contents,
&not_statement.variables,
)
self.populate_job_process(ctx, job, proc, &not_statement.negated_statement)
}
/// Creates a 'normal' (non-block) process.
@@ -1583,16 +1592,24 @@ fn run_1_job(
// However, if there are no redirections, then we can just jump into the block directly, which
// is significantly faster.
if self.job_is_simple_block(job_node) {
let mut block = None;
let mut result =
self.apply_variable_assignments(ctx, None, &job_node.variables, &mut block);
let variable_statement = &job_node.statement2;
let mut block = variable_statement.statement.is_some().then_some(None);
let mut result = self.apply_variable_assignments(
ctx,
None,
&variable_statement.variables,
block.as_mut(),
);
let Some(statement) = variable_statement.statement.as_ref() else {
return result;
};
let _scope = ScopeGuard::new((), |()| {
if let Some(block) = block {
if let Some(Some(block)) = block {
ctx.parser().pop_block(block);
}
});
let specific_statement = &job_node.statement.contents;
let specific_statement = &statement.contents;
assert!(specific_statement_type_is_redirectable_block(
specific_statement
));
@@ -1819,13 +1836,7 @@ fn populate_job_from_job_node(
// Create processes. Each one may fail.
let mut processes = ProcessList::new();
processes.push(Box::new(Process::new()));
let mut result = self.populate_job_process(
ctx,
j,
&mut processes[0],
&job_node.statement,
&job_node.variables,
);
let mut result = self.populate_job_process(ctx, j, &mut processes[0], &job_node.statement2);
// Construct process_ts for job continuations (pipelines).
for jc in &job_node.continuation {
@@ -1859,13 +1870,8 @@ fn populate_job_from_job_node(
// Store the new process (and maybe with an error).
processes.push(Box::new(Process::new()));
result = self.populate_job_process(
ctx,
j,
processes.last_mut().unwrap(),
&jc.statement,
&jc.variables,
);
result =
self.populate_job_process(ctx, j, processes.last_mut().unwrap(), &jc.statement2);
}
// Inform our processes of who is first and last
@@ -2016,18 +2022,29 @@ fn job_node_wants_timing(job_node: &ast::JobPipeline) -> bool {
if ns.time.is_some() {
return true;
}
stat = &ns.contents;
stat = match ns.negated_statement.statement.as_ref() {
Some(s) => s,
None => return false,
};
}
_ => return false,
}
};
// Do we have a 'not time ...' anywhere in our pipeline?
if is_timed_not_statement(&job_node.statement) {
if job_node
.statement3()
.map(is_timed_not_statement)
.unwrap_or_default()
{
return true;
}
for jc in &job_node.continuation {
if is_timed_not_statement(&jc.statement) {
if jc
.statement()
.map(is_timed_not_statement)
.unwrap_or_default()
{
return true;
}
}

View File

@@ -28,7 +28,7 @@ pub struct ParseToken {
/// Hackish: if TOK_END, whether the source is a newline.
pub is_newline: bool,
// Hackish: whether this token is a string like FOO=bar
pub may_be_variable_assignment: bool,
pub is_variable_assignment: bool,
/// If this is a tokenizer error, that error.
pub tok_error: TokenizerError,
source_start: SourceOffset,
@@ -43,7 +43,7 @@ pub fn new(typ: ParseTokenType) -> Self {
has_dash_prefix: false,
is_help_argument: false,
is_newline: false,
may_be_variable_assignment: false,
is_variable_assignment: false,
tok_error: TokenizerError::none,
source_start: SOURCE_OFFSET_INVALID.try_into().unwrap(),
source_length: 0,

View File

@@ -1201,7 +1201,12 @@ pub fn parse_util_detect_errors_in_ast(
if let Some(jc) = node.as_job_continuation() {
// Somewhat clumsy way of checking for a statement without source in a pipeline.
// See if our pipe has source but our statement does not.
if jc.pipe.has_source() && jc.statement.try_source_range().is_none() {
if jc.pipe.has_source()
&& jc
.statement()
.and_then(|stmt| stmt.try_source_range())
.is_none()
{
has_unclosed_pipe = true;
}
} else if let Some(job_conjunction) = node.as_job_conjunction() {
@@ -1627,7 +1632,8 @@ fn detect_errors_in_decorated_statement(
// Check our pipeline position.
let pipe_pos = if job.continuation.is_empty() {
PipelinePosition::none
} else if job.statement.pointer_eq(st) {
} else if job.statement3().unwrap().pointer_eq(st) {
// TODO(posix_mode) unwrap
PipelinePosition::first
} else {
PipelinePosition::subsequent

View File

@@ -41,6 +41,11 @@ fn detect_argument_errors(src: &str) -> Result<(), ParserTestErrorBits> {
parse_util_detect_errors_in_argument(first_arg, first_arg.source(&src), &mut errors)
}
assert!(
detect_errors!("true && ") == Err(ParserTestErrorBits::INCOMPLETE),
"unterminated conjunction not reported properly"
);
// Testing block nesting
assert!(
detect_errors!("if; end").is_err(),
@@ -547,13 +552,9 @@ fn test_new_parser_ad_hoc() {
// The bug was that "a=" would produce an error but not be consumed,
// leading to an infinite loop.
// By itself it should produce an error.
let ast = Ast::parse(L!("a="), ParseTreeFlags::default(), None);
assert!(ast.errored());
assert!(!ast.errored());
// If we are leaving things unterminated, this should not produce an error.
// i.e. when typing "a=" at the command line, it should be treated as valid
// because we don't want to color it as an error.
let ast = Ast::parse(L!("a="), ParseTreeFlags::LEAVE_UNTERMINATED, None);
assert!(!ast.errored());
@@ -617,8 +618,6 @@ macro_rules! validate {
validate!("begin ; }", ParseErrorCode::unbalancing_brace);
validate!("true | and", ParseErrorCode::andor_in_pipeline);
validate!("a=", ParseErrorCode::bare_variable_assignment);
}
#[test]