1use super::*;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub enum ConditionalOperatorFamily {
5 StringUnary,
6 StringBinary,
7 Regex,
8 Logical,
9 Other,
10}
11
12#[derive(Debug, Clone, Copy)]
13pub struct ConditionalOperandFact<'a> {
14 expression: &'a ConditionalExpr,
15 class: TestOperandClass,
16 word: Option<&'a Word>,
17 word_classification: Option<WordClassification>,
18}
19
20impl<'a> ConditionalOperandFact<'a> {
21 pub fn expression(&self) -> &'a ConditionalExpr {
22 self.expression
23 }
24
25 pub fn class(&self) -> TestOperandClass {
26 self.class
27 }
28
29 pub fn word(&self) -> Option<&'a Word> {
30 self.word
31 }
32
33 pub fn is_optional_first_positional_parameter(self) -> bool {
34 self.word
35 .is_some_and(word_is_optional_first_positional_parameter)
36 }
37
38 pub fn word_classification(&self) -> Option<WordClassification> {
39 self.word_classification
40 }
41
42 pub fn quote(&self) -> Option<WordQuote> {
43 self.word_classification
44 .map(|classification| classification.quote)
45 }
46}
47
48pub(crate) fn word_is_optional_first_positional_parameter(word: &Word) -> bool {
49 let [part] = word.parts.as_slice() else {
50 return false;
51 };
52
53 word_part_is_optional_first_positional_parameter(&part.kind)
54}
55
56pub(crate) fn word_part_is_optional_first_positional_parameter(part: &WordPart) -> bool {
57 match part {
58 WordPart::Variable(name) => name.as_str() == "1",
59 WordPart::DoubleQuoted { parts, .. } => {
60 let [part] = parts.as_slice() else {
61 return false;
62 };
63 word_part_is_optional_first_positional_parameter(&part.kind)
64 }
65 WordPart::Parameter(parameter) => parameter_is_optional_first_positional(parameter),
66 WordPart::ParameterExpansion {
67 reference,
68 operator,
69 ..
70 } => {
71 reference_is_first_positional_parameter(reference)
72 && matches!(operator.as_ref(), ParameterOp::UseDefault)
73 }
74 WordPart::Literal(_)
75 | WordPart::ZshQualifiedGlob(_)
76 | WordPart::SingleQuoted { .. }
77 | WordPart::CommandSubstitution { .. }
78 | WordPart::ArithmeticExpansion { .. }
79 | WordPart::Length(_)
80 | WordPart::ArrayAccess(_)
81 | WordPart::ArrayLength(_)
82 | WordPart::ArrayIndices(_)
83 | WordPart::Substring { .. }
84 | WordPart::ArraySlice { .. }
85 | WordPart::IndirectExpansion { .. }
86 | WordPart::PrefixMatch { .. }
87 | WordPart::ProcessSubstitution { .. }
88 | WordPart::Transformation { .. } => false,
89 }
90}
91
92pub(crate) fn parameter_is_optional_first_positional(parameter: &ParameterExpansion) -> bool {
93 match ¶meter.syntax {
94 ParameterExpansionSyntax::Bourne(BourneParameterExpansion::Access { reference }) => {
95 reference_is_first_positional_parameter(reference)
96 }
97 ParameterExpansionSyntax::Bourne(BourneParameterExpansion::Operation {
98 reference,
99 operator,
100 ..
101 }) => {
102 reference_is_first_positional_parameter(reference)
103 && matches!(operator.as_ref(), ParameterOp::UseDefault)
104 }
105 ParameterExpansionSyntax::Zsh(syntax) => {
106 matches!(
107 &syntax.target,
108 ZshExpansionTarget::Reference(reference)
109 if reference_is_first_positional_parameter(reference)
110 ) && match &syntax.operation {
111 None => true,
112 Some(ZshExpansionOperation::Defaulting { kind, .. }) => {
113 matches!(kind, shuck_ast::ZshDefaultingOp::UseDefault)
114 }
115 Some(
116 ZshExpansionOperation::PatternOperation { .. }
117 | ZshExpansionOperation::TrimOperation { .. }
118 | ZshExpansionOperation::ReplacementOperation { .. }
119 | ZshExpansionOperation::Slice { .. }
120 | ZshExpansionOperation::Unknown { .. },
121 ) => false,
122 }
123 }
124 ParameterExpansionSyntax::Bourne(
125 BourneParameterExpansion::Length { .. }
126 | BourneParameterExpansion::Indices { .. }
127 | BourneParameterExpansion::Indirect { .. }
128 | BourneParameterExpansion::PrefixMatch { .. }
129 | BourneParameterExpansion::Slice { .. }
130 | BourneParameterExpansion::Transformation { .. },
131 ) => false,
132 }
133}
134
135pub(crate) fn reference_is_first_positional_parameter(reference: &VarRef) -> bool {
136 reference.subscript.is_none() && reference.name.as_str() == "1"
137}
138
139#[derive(Debug, Clone, Copy)]
140pub struct ConditionalBareWordFact<'a> {
141 expression: &'a ConditionalExpr,
142 operand: ConditionalOperandFact<'a>,
143}
144
145impl<'a> ConditionalBareWordFact<'a> {
146 pub fn expression(&self) -> &'a ConditionalExpr {
147 self.expression
148 }
149
150 pub fn operand(&self) -> ConditionalOperandFact<'a> {
151 self.operand
152 }
153}
154
155#[derive(Debug, Clone, Copy)]
156pub struct ConditionalUnaryFact<'a> {
157 expression: &'a ConditionalExpr,
158 op: ConditionalUnaryOp,
159 operator_family: ConditionalOperatorFamily,
160 operand: ConditionalOperandFact<'a>,
161}
162
163impl<'a> ConditionalUnaryFact<'a> {
164 pub fn expression(&self) -> &'a ConditionalExpr {
165 self.expression
166 }
167
168 pub fn operator_span(&self) -> Span {
169 let ConditionalExpr::Unary(expression) = self.expression else {
170 unreachable!("conditional unary fact should wrap a unary expression");
171 };
172
173 expression.op_span
174 }
175
176 pub fn op(&self) -> ConditionalUnaryOp {
177 self.op
178 }
179
180 pub fn is_empty_string_test(&self) -> bool {
181 self.operator_family == ConditionalOperatorFamily::StringUnary
182 && self.op == ConditionalUnaryOp::EmptyString
183 }
184
185 pub fn operator_family(&self) -> ConditionalOperatorFamily {
186 self.operator_family
187 }
188
189 pub fn operand(&self) -> ConditionalOperandFact<'a> {
190 self.operand
191 }
192}
193
194#[derive(Debug, Clone, Copy)]
195pub struct ConditionalBinaryFact<'a> {
196 expression: &'a ConditionalExpr,
197 op: ConditionalBinaryOp,
198 operator_family: ConditionalOperatorFamily,
199 left: ConditionalOperandFact<'a>,
200 right: ConditionalOperandFact<'a>,
201}
202
203impl<'a> ConditionalBinaryFact<'a> {
204 pub fn expression(&self) -> &'a ConditionalExpr {
205 self.expression
206 }
207
208 pub fn operator_span(&self) -> Span {
209 let ConditionalExpr::Binary(expression) = self.expression else {
210 unreachable!("conditional binary fact should wrap a binary expression");
211 };
212
213 expression.op_span
214 }
215
216 pub fn op(&self) -> ConditionalBinaryOp {
217 self.op
218 }
219
220 pub fn operator_family(&self) -> ConditionalOperatorFamily {
221 self.operator_family
222 }
223
224 pub fn left(&self) -> ConditionalOperandFact<'a> {
225 self.left
226 }
227
228 pub fn right(&self) -> ConditionalOperandFact<'a> {
229 self.right
230 }
231}
232
233#[derive(Debug, Clone, Copy)]
234pub enum ConditionalNodeFact<'a> {
235 BareWord(ConditionalBareWordFact<'a>),
236 Unary(ConditionalUnaryFact<'a>),
237 Binary(ConditionalBinaryFact<'a>),
238 Other(&'a ConditionalExpr),
239}
240
241impl<'a> ConditionalNodeFact<'a> {
242 pub fn expression(&self) -> &'a ConditionalExpr {
243 match self {
244 Self::BareWord(fact) => fact.expression(),
245 Self::Unary(fact) => fact.expression(),
246 Self::Binary(fact) => fact.expression(),
247 Self::Other(expression) => expression,
248 }
249 }
250}
251
252#[derive(Debug, Clone, PartialEq, Eq)]
253pub struct ConditionalMixedLogicalOperatorFact {
254 operator_span: Span,
255 grouped_subexpression_spans: Box<[Span]>,
256}
257
258impl ConditionalMixedLogicalOperatorFact {
259 pub fn operator_span(&self) -> Span {
260 self.operator_span
261 }
262
263 pub fn grouped_subexpression_spans(&self) -> &[Span] {
264 &self.grouped_subexpression_spans
265 }
266}
267
268#[derive(Debug, Clone)]
269pub struct ConditionalFact<'a> {
270 nodes: Box<[ConditionalNodeFact<'a>]>,
271 mixed_logical_operators: Box<[ConditionalMixedLogicalOperatorFact]>,
272}
273
274impl<'a> ConditionalFact<'a> {
275 pub fn expression(&self) -> &'a ConditionalExpr {
276 self.root().expression()
277 }
278
279 pub fn root(&self) -> &ConditionalNodeFact<'a> {
280 &self.nodes[0]
281 }
282
283 pub fn nodes(&self) -> &[ConditionalNodeFact<'a>] {
284 &self.nodes
285 }
286
287 pub fn mixed_logical_operators(&self) -> &[ConditionalMixedLogicalOperatorFact] {
288 &self.mixed_logical_operators
289 }
290
291 pub fn regex_nodes(&self) -> impl Iterator<Item = &ConditionalBinaryFact<'a>> + '_ {
292 self.nodes.iter().filter_map(|node| match node {
293 ConditionalNodeFact::Binary(fact)
294 if fact.operator_family() == ConditionalOperatorFamily::Regex =>
295 {
296 Some(fact)
297 }
298 _ => None,
299 })
300 }
301}
302
303#[derive(Debug, Clone, Copy)]
304pub(crate) struct ConditionalExpressionVisit<'a> {
305 expression: &'a ConditionalExpr,
306 parent_in_same_logical_group: bool,
307}
308
309impl<'a> ConditionalExpressionVisit<'a> {
310 pub(crate) fn new(expression: &'a ConditionalExpr, parent_in_same_logical_group: bool) -> Self {
311 Self {
312 expression,
313 parent_in_same_logical_group,
314 }
315 }
316}
317
318pub(crate) fn collect_command_substitution_command_span(
319 command: &Command,
320 source: &str,
321 spans: &mut Vec<Span>,
322) {
323 if let Command::Simple(command) = command
324 && command.args.is_empty()
325 && command_name_is_plain_command_substitution(&command.name, source)
326 {
327 spans.push(command.name.span);
328 }
329}
330
331pub(crate) fn collect_c107_status_spans_in_simple_test(
332 command: &shuck_ast::SimpleCommand,
333 source: &str,
334 spans: &mut Vec<Span>,
335) {
336 if static_word_text(&command.name, source).as_deref() != Some("[") {
337 return;
338 }
339
340 let Some((closing_bracket, operands)) = command.args.split_last() else {
341 return;
342 };
343 if static_word_text(closing_bracket, source).as_deref() != Some("]") {
344 return;
345 }
346
347 let operands = operands.iter().collect::<Vec<_>>();
348 let effective_operand_offset = simple_test_effective_operand_offset(&operands, source);
349 let effective_operands = &operands[effective_operand_offset..];
350 if effective_operands.len() != 3 {
351 return;
352 }
353
354 let Some(operator) = static_word_text(effective_operands[1], source) else {
355 return;
356 };
357 if !matches!(
358 operator.as_ref(),
359 "=" | "==" | "!=" | "-eq" | "-ne" | "-lt" | "-le" | "-gt" | "-ge"
360 ) {
361 return;
362 }
363
364 let left_status = c107_status_word_span(effective_operands[0]);
365 let right_status = c107_status_word_span(effective_operands[2]);
366 let left_zero = c107_word_is_zero_literal(effective_operands[0], source);
367 let right_zero = c107_word_is_zero_literal(effective_operands[2], source);
368
369 if let Some(span) = left_status.filter(|_| right_zero) {
370 spans.push(span);
371 } else if let Some(span) = right_status.filter(|_| left_zero) {
372 spans.push(span);
373 }
374}
375
376pub(crate) fn collect_c107_status_spans_in_conditional_expr(
377 expression: &ConditionalExpr,
378 source: &str,
379 spans: &mut Vec<Span>,
380) {
381 if let Some(span) = c107_conditional_expr_status_span(expression, source) {
382 spans.push(span);
383 }
384}
385
386pub(crate) fn c107_conditional_expr_status_span(
387 expression: &ConditionalExpr,
388 source: &str,
389) -> Option<Span> {
390 match expression {
391 ConditionalExpr::Binary(expression) => {
392 if matches!(
393 expression.op,
394 ConditionalBinaryOp::And | ConditionalBinaryOp::Or
395 ) {
396 return None;
397 }
398 if !matches!(
399 expression.op,
400 ConditionalBinaryOp::ArithmeticEq
401 | ConditionalBinaryOp::ArithmeticNe
402 | ConditionalBinaryOp::ArithmeticLe
403 | ConditionalBinaryOp::ArithmeticGe
404 | ConditionalBinaryOp::ArithmeticLt
405 | ConditionalBinaryOp::ArithmeticGt
406 | ConditionalBinaryOp::PatternEqShort
407 | ConditionalBinaryOp::PatternEq
408 | ConditionalBinaryOp::PatternNe
409 ) {
410 return None;
411 }
412
413 let left_status = c107_conditional_operand_status_span(&expression.left);
414 let right_status = c107_conditional_operand_status_span(&expression.right);
415 let left_zero = c107_conditional_expr_is_zero_literal(&expression.left, source);
416 let right_zero = c107_conditional_expr_is_zero_literal(&expression.right, source);
417
418 left_status
419 .filter(|_| right_zero)
420 .or_else(|| right_status.filter(|_| left_zero))
421 }
422 ConditionalExpr::Unary(expression) => {
423 c107_conditional_expr_status_span(&expression.expr, source)
424 }
425 ConditionalExpr::Parenthesized(expression) => {
426 c107_conditional_expr_status_span(&expression.expr, source)
427 }
428 ConditionalExpr::Word(_)
429 | ConditionalExpr::Pattern(_)
430 | ConditionalExpr::Regex(_)
431 | ConditionalExpr::VarRef(_) => None,
432 }
433}
434
435pub(crate) fn c107_conditional_operand_status_span(expression: &ConditionalExpr) -> Option<Span> {
436 match expression {
437 ConditionalExpr::Word(word) | ConditionalExpr::Regex(word) => c107_status_word_span(word),
438 ConditionalExpr::Pattern(pattern) => {
439 pattern.parts.iter().find_map(|part| match &part.kind {
440 PatternPart::Word(word) => c107_status_word_span(word),
441 PatternPart::Literal(_)
442 | PatternPart::AnyString
443 | PatternPart::AnyChar
444 | PatternPart::CharClass(_)
445 | PatternPart::Group { .. } => None,
446 })
447 }
448 ConditionalExpr::VarRef(reference) => {
449 (reference.name.as_str() == "?").then_some(reference.span)
450 }
451 ConditionalExpr::Parenthesized(expression) => {
452 c107_conditional_operand_status_span(&expression.expr)
453 }
454 ConditionalExpr::Unary(expression) => {
455 c107_conditional_operand_status_span(&expression.expr)
456 }
457 ConditionalExpr::Binary(_) => None,
458 }
459}
460
461pub(crate) fn c107_conditional_expr_is_zero_literal(
462 expression: &ConditionalExpr,
463 source: &str,
464) -> bool {
465 match expression {
466 ConditionalExpr::Word(word) | ConditionalExpr::Regex(word) => {
467 c107_word_is_zero_literal(word, source)
468 }
469 ConditionalExpr::Pattern(pattern) => c107_pattern_is_zero_literal(pattern, source),
470 ConditionalExpr::Parenthesized(expression) => {
471 c107_conditional_expr_is_zero_literal(&expression.expr, source)
472 }
473 ConditionalExpr::Unary(expression) => {
474 c107_conditional_expr_is_zero_literal(&expression.expr, source)
475 }
476 ConditionalExpr::VarRef(_) | ConditionalExpr::Binary(_) => false,
477 }
478}
479
480pub(crate) fn collect_c107_status_spans_in_arithmetic_command(
481 command: &shuck_ast::ArithmeticCommand,
482 source: &str,
483 spans: &mut Vec<Span>,
484) {
485 let Some(expression) = &command.expr_ast else {
486 return;
487 };
488
489 if let Some(span) = c107_arithmetic_expr_status_span(expression, source) {
490 spans.push(span);
491 }
492}
493
494pub(crate) fn c107_arithmetic_expr_status_span(
495 expression: &shuck_ast::ArithmeticExprNode,
496 source: &str,
497) -> Option<Span> {
498 match &expression.kind {
499 shuck_ast::ArithmeticExpr::Parenthesized { expression } => {
500 c107_arithmetic_expr_status_span(expression, source)
501 }
502 shuck_ast::ArithmeticExpr::Unary { expr, .. } => {
503 c107_arithmetic_expr_status_span(expr, source)
504 }
505 shuck_ast::ArithmeticExpr::Binary { left, op, right } => {
506 if !matches!(
507 op,
508 shuck_ast::ArithmeticBinaryOp::LessThan
509 | shuck_ast::ArithmeticBinaryOp::LessThanOrEqual
510 | shuck_ast::ArithmeticBinaryOp::GreaterThan
511 | shuck_ast::ArithmeticBinaryOp::GreaterThanOrEqual
512 | shuck_ast::ArithmeticBinaryOp::Equal
513 | shuck_ast::ArithmeticBinaryOp::NotEqual
514 ) {
515 return None;
516 }
517
518 let left_status = c107_arithmetic_operand_status_span(left);
519 let right_status = c107_arithmetic_operand_status_span(right);
520 let left_zero = c107_arithmetic_expr_is_zero_literal(left, source);
521 let right_zero = c107_arithmetic_expr_is_zero_literal(right, source);
522
523 left_status
524 .filter(|_| right_zero)
525 .or_else(|| right_status.filter(|_| left_zero))
526 }
527 _ => None,
528 }
529}
530
531pub(crate) fn c107_arithmetic_operand_status_span(
532 expression: &shuck_ast::ArithmeticExprNode,
533) -> Option<Span> {
534 match &expression.kind {
535 shuck_ast::ArithmeticExpr::ShellWord(word) => c107_status_word_span(word),
536 shuck_ast::ArithmeticExpr::Parenthesized { expression } => {
537 c107_arithmetic_operand_status_span(expression)
538 }
539 shuck_ast::ArithmeticExpr::Unary { expr, .. } => c107_arithmetic_operand_status_span(expr),
540 _ => None,
541 }
542}
543
544pub(crate) fn c107_arithmetic_expr_is_zero_literal(
545 expression: &shuck_ast::ArithmeticExprNode,
546 source: &str,
547) -> bool {
548 match &expression.kind {
549 shuck_ast::ArithmeticExpr::Number(text) => text.slice(source).trim() == "0",
550 shuck_ast::ArithmeticExpr::ShellWord(word) => c107_word_is_zero_literal(word, source),
551 shuck_ast::ArithmeticExpr::Parenthesized { expression } => {
552 c107_arithmetic_expr_is_zero_literal(expression, source)
553 }
554 shuck_ast::ArithmeticExpr::Unary { expr, .. } => {
555 c107_arithmetic_expr_is_zero_literal(expr, source)
556 }
557 _ => false,
558 }
559}
560
561pub(crate) fn c107_status_word_span(word: &Word) -> Option<Span> {
562 word_is_standalone_status_capture(word).then_some(word.span)
563}
564
565pub(crate) fn c107_word_is_zero_literal(word: &Word, source: &str) -> bool {
566 static_word_text(word, source).as_deref() == Some("0")
567}
568
569pub(crate) fn c107_pattern_is_zero_literal(pattern: &Pattern, source: &str) -> bool {
570 match pattern.parts.as_slice() {
571 [part] => match &part.kind {
572 PatternPart::Literal(text) => text.as_str(source, part.span) == "0",
573 PatternPart::Word(word) => c107_word_is_zero_literal(word, source),
574 PatternPart::AnyString
575 | PatternPart::AnyChar
576 | PatternPart::CharClass(_)
577 | PatternPart::Group { .. } => false,
578 },
579 _ => false,
580 }
581}
582
583pub(crate) fn collect_condition_status_capture_from_body(
584 condition: &StmtSeq,
585 body: &StmtSeq,
586 source: &str,
587 spans: &mut Vec<Span>,
588) {
589 if !condition_terminals_are_test_commands(condition, source) {
590 return;
591 }
592
593 let Some(first_stmt) = body.first() else {
594 return;
595 };
596
597 let mut stmt_spans = Vec::new();
598 collect_status_parameter_spans_in_stmt(first_stmt, source, &mut stmt_spans);
599 if stmt_spans.is_empty()
600 || branch_body_status_capture_is_exempt(first_stmt, body, source, &stmt_spans)
601 {
602 return;
603 }
604
605 spans.extend(stmt_spans);
606}
607
608pub(crate) fn collect_condition_status_capture_from_sequence(
609 commands: &StmtSeq,
610 source: &str,
611 spans: &mut Vec<Span>,
612) {
613 let body_shape = BodyShapeAnalyzer::new(source);
614 let mut segment_start = 0;
615 for (index, stmt) in commands.iter().enumerate() {
616 if !stmt_starts_sequence_barrier(stmt) {
617 continue;
618 }
619
620 collect_condition_status_capture_from_sequence_segment(
621 &commands.as_slice()[segment_start..index],
622 source,
623 spans,
624 body_shape.sequence_tail_contains_nested_test_command(&commands.as_slice()[index..]),
625 segment_start > 0,
626 );
627 segment_start = index + 1;
628 }
629 collect_condition_status_capture_from_sequence_segment(
630 &commands.as_slice()[segment_start..],
631 source,
632 spans,
633 false,
634 segment_start > 0,
635 );
636
637 for (index, stmt) in commands.iter().enumerate() {
638 if !body_shape.sequence_tail_contains_nested_test_command(&commands.as_slice()[index + 1..])
639 {
640 continue;
641 }
642
643 let Command::Compound(CompoundCommand::Subshell(body) | CompoundCommand::BraceGroup(body)) =
644 &stmt.command
645 else {
646 continue;
647 };
648
649 body_shape.collect_status_test_followup_chains_with_sibling_tail(body, spans);
650 }
651}
652
653pub(crate) fn collect_condition_status_capture_from_direct_body_sequences(
654 command: &Command,
655 source: &str,
656 spans: &mut Vec<Span>,
657) {
658 match command {
659 Command::Compound(command) => match command {
660 CompoundCommand::If(command) => {
661 collect_condition_status_capture_from_sequence(&command.then_branch, source, spans);
662 for (_, branch) in &command.elif_branches {
663 collect_condition_status_capture_from_sequence(branch, source, spans);
664 }
665 if let Some(branch) = &command.else_branch {
666 collect_condition_status_capture_from_sequence(branch, source, spans);
667 }
668 }
669 CompoundCommand::While(command) => {
670 collect_condition_status_capture_from_sequence(&command.body, source, spans);
671 }
672 CompoundCommand::Until(command) => {
673 collect_condition_status_capture_from_sequence(&command.body, source, spans);
674 }
675 CompoundCommand::For(command) => {
676 collect_condition_status_capture_from_sequence(&command.body, source, spans);
677 }
678 CompoundCommand::Select(command) => {
679 collect_condition_status_capture_from_sequence(&command.body, source, spans);
680 }
681 CompoundCommand::BraceGroup(body) | CompoundCommand::Subshell(body) => {
682 collect_condition_status_capture_from_sequence(body, source, spans);
683 }
684 CompoundCommand::Time(_) => {}
685 CompoundCommand::Always(command) => {
686 collect_condition_status_capture_from_sequence(&command.body, source, spans);
687 collect_condition_status_capture_from_sequence(&command.always_body, source, spans);
688 }
689 CompoundCommand::Case(_)
690 | CompoundCommand::Conditional(_)
691 | CompoundCommand::Repeat(_)
692 | CompoundCommand::Foreach(_)
693 | CompoundCommand::ArithmeticFor(_)
694 | CompoundCommand::Arithmetic(_)
695 | CompoundCommand::Coproc(_) => {}
696 },
697 Command::Simple(_)
698 | Command::Builtin(_)
699 | Command::Decl(_)
700 | Command::Binary(_)
701 | Command::Function(_)
702 | Command::AnonymousFunction(_) => {}
703 }
704}
705
706pub(crate) fn collect_condition_status_capture_from_sequence_segment(
707 commands: &[Stmt],
708 source: &str,
709 spans: &mut Vec<Span>,
710 trailing_tail_has_test: bool,
711 has_prior_barrier: bool,
712) {
713 let tail_contains_test = sequence_tail_test_index(commands, source);
714 let mut recent = RecentSequenceStatus::default();
715 let body_topology = BodyTopology::from_statements(commands);
716 for (index, previous, current) in body_topology.indexed_sibling_pairs() {
717 if let Some(before_previous) = body_topology.previous_sibling(index) {
718 recent.push(before_previous, source);
719 }
720 let tail_has_test = tail_contains_test.get(index + 2).copied().unwrap_or(false);
721 let effective_tail_has_test = trailing_tail_has_test || tail_has_test;
722 let in_tbegin_region = recent.seen_tbegin;
723
724 if stmt_is_assignment_only_unquoted_status_capture(current) {
725 if stmt_is_standalone_non_status_test_command(previous, source)
726 && in_tbegin_region
727 && recent.status_capture_count >= 2
728 && tail_has_test
729 {
730 collect_status_parameter_spans_in_stmt(current, source, spans);
731 }
732 continue;
733 }
734
735 if !stmt_is_plain_command_with_standalone_status_argument(current) {
736 continue;
737 }
738
739 if stmt_is_status_based_test_command(previous, source) {
740 if (!has_prior_barrier && effective_tail_has_test) || recent.contains_status_based_test
741 {
742 collect_status_parameter_spans_in_stmt(current, source, spans);
743 }
744 continue;
745 }
746
747 if !stmt_is_standalone_non_status_test_command(previous, source) {
748 continue;
749 }
750
751 if in_tbegin_region && recent.has_prior_non_status_capture_pair {
752 continue;
753 }
754
755 if effective_tail_has_test {
756 collect_status_parameter_spans_in_stmt(current, source, spans);
757 }
758 }
759}
760
761#[derive(Default)]
762pub(crate) struct RecentSequenceStatus<'a> {
763 seen_tbegin: bool,
764 status_capture_count: usize,
765 contains_status_based_test: bool,
766 has_prior_non_status_capture_pair: bool,
767 last_stmt: Option<&'a Stmt>,
768}
769
770impl<'a> RecentSequenceStatus<'a> {
771 fn push(&mut self, stmt: &'a Stmt, source: &str) {
772 if stmt_is_named_simple_command(stmt, source, "tbegin") {
773 self.seen_tbegin = true;
774 self.status_capture_count = 0;
775 self.contains_status_based_test = false;
776 self.has_prior_non_status_capture_pair = false;
777 self.last_stmt = None;
778 return;
779 }
780
781 let contains_status_capture = stmt_contains_status_capture(stmt, source);
782 if self
783 .last_stmt
784 .is_some_and(|previous| stmt_is_standalone_non_status_test_command(previous, source))
785 && contains_status_capture
786 {
787 self.has_prior_non_status_capture_pair = true;
788 }
789 if contains_status_capture {
790 self.status_capture_count += 1;
791 }
792 if stmt_is_status_based_test_command(stmt, source) {
793 self.contains_status_based_test = true;
794 }
795 self.last_stmt = Some(stmt);
796 }
797}
798
799pub(crate) fn sequence_tail_test_index(commands: &[Stmt], source: &str) -> Vec<bool> {
800 let mut suffix = vec![false; commands.len() + 1];
801 for (index, stmt) in commands.iter().enumerate().rev() {
802 suffix[index] = suffix[index + 1] || stmt_terminals_are_test_commands(stmt, source);
803 }
804 suffix
805}
806
807pub(crate) fn branch_body_status_capture_is_exempt(
808 first_stmt: &Stmt,
809 body: &StmtSeq,
810 source: &str,
811 stmt_spans: &[Span],
812) -> bool {
813 if branch_body_preserves_status_in_plain_assignment(first_stmt, body, source, stmt_spans) {
814 return true;
815 }
816
817 if branch_body_logs_status_then_immediately_exits(first_stmt, body, source, stmt_spans) {
818 return true;
819 }
820
821 false
822}
823
824pub(crate) fn branch_body_preserves_status_in_plain_assignment(
825 first_stmt: &Stmt,
826 _body: &StmtSeq,
827 source: &str,
828 stmt_spans: &[Span],
829) -> bool {
830 let Some(_) = stmt_plain_assignment_only_name(first_stmt) else {
831 return false;
832 };
833 if stmt_contains_unquoted_standalone_status_capture(first_stmt, source) || stmt_spans.is_empty()
834 {
835 return false;
836 }
837 true
838}
839
840pub(crate) fn branch_body_logs_status_then_immediately_exits(
841 first_stmt: &Stmt,
842 body: &StmtSeq,
843 source: &str,
844 stmt_spans: &[Span],
845) -> bool {
846 if stmt_spans.is_empty() || stmt_contains_unquoted_standalone_status_capture(first_stmt, source)
847 {
848 return false;
849 }
850
851 body.iter()
852 .nth(1)
853 .is_some_and(stmt_is_exit_or_return_builtin)
854}
855
856pub(crate) fn collect_precise_function_return_guard_suppressions_in_seq(
857 commands: &StmtSeq,
858 source: &str,
859 spans: &mut Vec<Span>,
860 in_function_like_body: bool,
861) {
862 let stmts = commands.as_slice();
863 let body_shape = BodyShapeAnalyzer::new(source);
864 for (index, stmt) in stmts.iter().enumerate() {
865 if in_function_like_body && stmt_is_test_return_status_guard(stmt, source) {
866 let previous_non_test_guard =
867 index > 0 && stmt_is_non_test_return_status_guard(&stmts[index - 1], source);
868 let later_unary_guard = stmts[index + 1..]
869 .iter()
870 .any(|stmt| stmt_is_unary_test_return_status_guard(stmt, source));
871 let status_accumulator_guard =
872 body_shape.stmt_starts_status_accumulator_return_guard(index, stmts);
873 if status_accumulator_guard
874 || (stmt_is_unary_test_return_status_guard(stmt, source)
875 && (previous_non_test_guard || later_unary_guard))
876 {
877 collect_status_parameter_spans_in_return_guard_stmt(stmt, source, spans);
878 }
879 }
880 }
881}
882
883pub(crate) fn collect_precise_function_return_guard_suppressions_from_direct_body_sequences(
884 command: &Command,
885 source: &str,
886 spans: &mut Vec<Span>,
887 in_function_like_body: bool,
888) {
889 match command {
890 Command::Compound(command) => match command {
891 CompoundCommand::If(command) => {
892 collect_precise_function_return_guard_suppressions_in_seq(
893 &command.then_branch,
894 source,
895 spans,
896 in_function_like_body,
897 );
898 for (_, branch) in &command.elif_branches {
899 collect_precise_function_return_guard_suppressions_in_seq(
900 branch,
901 source,
902 spans,
903 in_function_like_body,
904 );
905 }
906 if let Some(branch) = &command.else_branch {
907 collect_precise_function_return_guard_suppressions_in_seq(
908 branch,
909 source,
910 spans,
911 in_function_like_body,
912 );
913 }
914 }
915 CompoundCommand::While(command) => {
916 collect_precise_function_return_guard_suppressions_in_seq(
917 &command.body,
918 source,
919 spans,
920 in_function_like_body,
921 );
922 }
923 CompoundCommand::Until(command) => {
924 collect_precise_function_return_guard_suppressions_in_seq(
925 &command.body,
926 source,
927 spans,
928 in_function_like_body,
929 );
930 }
931 CompoundCommand::For(command) => {
932 collect_precise_function_return_guard_suppressions_in_seq(
933 &command.body,
934 source,
935 spans,
936 in_function_like_body,
937 );
938 }
939 CompoundCommand::Select(command) => {
940 collect_precise_function_return_guard_suppressions_in_seq(
941 &command.body,
942 source,
943 spans,
944 in_function_like_body,
945 );
946 }
947 CompoundCommand::BraceGroup(body) | CompoundCommand::Subshell(body) => {
948 collect_precise_function_return_guard_suppressions_in_seq(
949 body,
950 source,
951 spans,
952 in_function_like_body,
953 );
954 }
955 CompoundCommand::Time(_) => {}
956 CompoundCommand::Always(command) => {
957 collect_precise_function_return_guard_suppressions_in_seq(
958 &command.body,
959 source,
960 spans,
961 in_function_like_body,
962 );
963 collect_precise_function_return_guard_suppressions_in_seq(
964 &command.always_body,
965 source,
966 spans,
967 in_function_like_body,
968 );
969 }
970 CompoundCommand::Case(command) => {
971 for case in &command.cases {
972 collect_precise_function_return_guard_suppressions_in_seq(
973 &case.body,
974 source,
975 spans,
976 in_function_like_body,
977 );
978 }
979 }
980 CompoundCommand::Conditional(_)
981 | CompoundCommand::Repeat(_)
982 | CompoundCommand::Foreach(_)
983 | CompoundCommand::ArithmeticFor(_)
984 | CompoundCommand::Arithmetic(_)
985 | CompoundCommand::Coproc(_) => {}
986 },
987 Command::Simple(_)
988 | Command::Builtin(_)
989 | Command::Decl(_)
990 | Command::Binary(_)
991 | Command::Function(_)
992 | Command::AnonymousFunction(_) => {}
993 }
994}
995
996pub(crate) fn condition_terminals_are_test_commands(condition: &StmtSeq, source: &str) -> bool {
997 condition
998 .last()
999 .is_some_and(|stmt| stmt_terminals_are_test_commands(stmt, source))
1000}
1001
1002pub(crate) fn stmt_is_standalone_non_status_test_command(stmt: &Stmt, source: &str) -> bool {
1003 stmt_terminals_are_test_commands(stmt, source) && !stmt_contains_status_capture(stmt, source)
1004}
1005
1006pub(crate) fn stmt_is_status_based_test_command(stmt: &Stmt, source: &str) -> bool {
1007 stmt_terminals_are_test_commands(stmt, source) && stmt_contains_status_capture(stmt, source)
1008}
1009
1010pub(crate) fn stmt_is_unary_test_return_status_guard(stmt: &Stmt, source: &str) -> bool {
1011 let Command::Binary(command) = &stmt.command else {
1012 return false;
1013 };
1014 matches!(command.op, BinaryOp::Or)
1015 && stmt_is_simple_unary_test_command(&command.left, source)
1016 && stmt_is_return_status_command(&command.right, source)
1017}
1018
1019pub(crate) fn stmt_is_test_return_status_guard(stmt: &Stmt, source: &str) -> bool {
1020 let Command::Binary(command) = &stmt.command else {
1021 return false;
1022 };
1023 matches!(command.op, BinaryOp::Or)
1024 && stmt_terminals_are_test_commands(&command.left, source)
1025 && stmt_is_return_status_command(&command.right, source)
1026}
1027
1028pub(crate) fn stmt_is_non_test_return_status_guard(stmt: &Stmt, source: &str) -> bool {
1029 let Command::Binary(command) = &stmt.command else {
1030 return false;
1031 };
1032 matches!(command.op, BinaryOp::Or)
1033 && !stmt_terminals_are_test_commands(&command.left, source)
1034 && stmt_is_return_status_command(&command.right, source)
1035}
1036
1037pub(crate) fn stmt_is_return_status_command(stmt: &Stmt, source: &str) -> bool {
1038 if stmt.negated || !stmt.redirects.is_empty() {
1039 return false;
1040 }
1041
1042 match &stmt.command {
1043 Command::Builtin(BuiltinCommand::Return(command)) => command
1044 .code
1045 .as_ref()
1046 .is_some_and(word_is_unquoted_standalone_status_capture),
1047 Command::Simple(command) => {
1048 static_word_text(&command.name, source).as_deref() == Some("return")
1049 && command.args.len() == 1
1050 && word_is_unquoted_standalone_status_capture(&command.args[0])
1051 }
1052 Command::Builtin(_)
1053 | Command::Decl(_)
1054 | Command::Binary(_)
1055 | Command::Compound(_)
1056 | Command::Function(_)
1057 | Command::AnonymousFunction(_) => false,
1058 }
1059}
1060
1061pub(crate) fn stmt_is_simple_unary_test_command(stmt: &Stmt, source: &str) -> bool {
1062 if stmt.negated {
1063 return false;
1064 }
1065
1066 match &stmt.command {
1067 Command::Compound(CompoundCommand::Conditional(command)) => {
1068 matches!(command.expression, ConditionalExpr::Unary(_))
1069 }
1070 Command::Simple(command) => {
1071 let Some(name) = static_word_text(&command.name, source) else {
1072 return false;
1073 };
1074 if !matches!(name.as_ref(), "[" | "test") {
1075 return false;
1076 }
1077 let Some((closing_bracket, operands)) = command.args.split_last() else {
1078 return false;
1079 };
1080 let operand_count = if name.as_ref() == "[" {
1081 if static_word_text(closing_bracket, source).as_deref() != Some("]") {
1082 return false;
1083 }
1084 operands.len()
1085 } else {
1086 command.args.len()
1087 };
1088 operand_count == 2
1089 }
1090 Command::Builtin(_)
1091 | Command::Decl(_)
1092 | Command::Binary(_)
1093 | Command::Compound(_)
1094 | Command::Function(_)
1095 | Command::AnonymousFunction(_) => false,
1096 }
1097}
1098
1099pub(crate) fn stmt_assignment_only_scalar_literal_name<'a>(
1100 stmt: &'a Stmt,
1101 source: &str,
1102 expected: &str,
1103) -> Option<&'a Name> {
1104 let name = stmt_plain_assignment_only_name(stmt)?;
1105 let Command::Simple(command) = &stmt.command else {
1106 return None;
1107 };
1108 let AssignmentValue::Scalar(word) = &command.assignments[0].value else {
1109 return None;
1110 };
1111 (static_word_text(word, source).as_deref() == Some(expected)).then_some(name)
1112}
1113
1114pub(crate) fn word_is_name_reference(word: &Word, name: &Name) -> bool {
1115 matches!(
1116 word.parts.as_slice(),
1117 [WordPartNode {
1118 kind: WordPart::Variable(var),
1119 ..
1120 }] if var == name
1121 ) || matches!(
1122 word.parts.as_slice(),
1123 [WordPartNode {
1124 kind: WordPart::Parameter(parameter),
1125 ..
1126 }] if matches!(
1127 parameter.bourne(),
1128 Some(BourneParameterExpansion::Access { reference })
1129 if reference.name == *name && reference.subscript.is_none()
1130 )
1131 )
1132}
1133
1134pub(crate) fn stmt_contains_status_capture(stmt: &Stmt, source: &str) -> bool {
1135 let mut spans = Vec::new();
1136 collect_status_parameter_spans_in_stmt(stmt, source, &mut spans);
1137 !spans.is_empty()
1138}
1139
1140pub(crate) fn stmt_contains_unquoted_standalone_status_capture(stmt: &Stmt, source: &str) -> bool {
1141 let mut spans = Vec::new();
1142 collect_unquoted_standalone_status_parameter_spans_in_stmt(stmt, source, &mut spans);
1143 !spans.is_empty()
1144}
1145
1146pub(crate) fn stmt_starts_sequence_barrier(stmt: &Stmt) -> bool {
1147 matches!(
1148 &stmt.command,
1149 Command::Compound(
1150 CompoundCommand::If(_)
1151 | CompoundCommand::Case(_)
1152 | CompoundCommand::For(_)
1153 | CompoundCommand::Select(_)
1154 | CompoundCommand::While(_)
1155 | CompoundCommand::Until(_)
1156 | CompoundCommand::Always(_)
1157 )
1158 )
1159}
1160
1161pub(crate) fn stmt_is_named_simple_command(stmt: &Stmt, source: &str, name: &str) -> bool {
1162 matches!(
1163 &stmt.command,
1164 Command::Simple(command)
1165 if static_word_text(&command.name, source).as_deref() == Some(name)
1166 )
1167}
1168
1169pub(crate) fn stmt_plain_assignment_only_name(stmt: &Stmt) -> Option<&Name> {
1170 if stmt.negated || !stmt.redirects.is_empty() {
1171 return None;
1172 }
1173
1174 let Command::Simple(command) = &stmt.command else {
1175 return None;
1176 };
1177 if !command.args.is_empty()
1178 || command.name.span.start.offset != command.name.span.end.offset
1179 || command.assignments.len() != 1
1180 {
1181 return None;
1182 }
1183
1184 Some(&command.assignments[0].target.name)
1185}
1186
1187pub(crate) fn stmt_is_assignment_only_unquoted_status_capture(stmt: &Stmt) -> bool {
1188 if stmt.negated || !stmt.redirects.is_empty() {
1189 return false;
1190 }
1191
1192 let Command::Simple(command) = &stmt.command else {
1193 return false;
1194 };
1195
1196 command.args.is_empty()
1197 && command.name.span.start.offset == command.name.span.end.offset
1198 && !command.assignments.is_empty()
1199 && command
1200 .assignments
1201 .iter()
1202 .all(|assignment| assignment_value_is_unquoted_status_capture(&assignment.value))
1203}
1204
1205pub(crate) fn assignment_value_is_unquoted_status_capture(value: &AssignmentValue) -> bool {
1206 match value {
1207 AssignmentValue::Scalar(word) => word_is_unquoted_standalone_status_capture(word),
1208 AssignmentValue::Compound(_) => false,
1209 }
1210}
1211
1212pub(crate) fn stmt_is_exit_or_return_builtin(stmt: &Stmt) -> bool {
1213 matches!(
1214 stmt.command,
1215 Command::Builtin(BuiltinCommand::Exit(_) | BuiltinCommand::Return(_))
1216 )
1217}
1218
1219pub(crate) fn stmt_is_plain_command_with_standalone_status_argument(stmt: &Stmt) -> bool {
1220 if stmt.negated || !stmt.redirects.is_empty() {
1221 return false;
1222 }
1223
1224 match &stmt.command {
1225 Command::Simple(command) => {
1226 command.name.span.start.offset != command.name.span.end.offset
1227 && (word_is_unquoted_standalone_status_capture(&command.name)
1228 || command
1229 .args
1230 .iter()
1231 .any(word_is_unquoted_standalone_status_capture))
1232 }
1233 Command::Builtin(command) => match command {
1234 BuiltinCommand::Return(command) => command
1235 .code
1236 .as_ref()
1237 .is_some_and(word_is_unquoted_standalone_status_capture),
1238 BuiltinCommand::Exit(command) => command
1239 .code
1240 .as_ref()
1241 .is_some_and(word_is_unquoted_standalone_status_capture),
1242 BuiltinCommand::Break(_) | BuiltinCommand::Continue(_) => false,
1243 },
1244 Command::Decl(_)
1245 | Command::Binary(_)
1246 | Command::Compound(_)
1247 | Command::Function(_)
1248 | Command::AnonymousFunction(_) => false,
1249 }
1250}
1251
1252pub(crate) fn word_is_unquoted_standalone_status_capture(word: &Word) -> bool {
1253 matches!(
1254 word.parts.as_slice(),
1255 [WordPartNode {
1256 kind: WordPart::Variable(name),
1257 ..
1258 }] if name.as_str() == "?"
1259 ) || matches!(
1260 word.parts.as_slice(),
1261 [WordPartNode {
1262 kind: WordPart::Parameter(parameter),
1263 ..
1264 }] if matches!(
1265 parameter.bourne(),
1266 Some(BourneParameterExpansion::Access { reference })
1267 if reference.name.as_str() == "?" && reference.subscript.is_none()
1268 )
1269 )
1270}
1271
1272pub(crate) fn stmt_terminals_are_test_commands(stmt: &Stmt, source: &str) -> bool {
1273 if stmt.negated {
1274 return false;
1275 }
1276
1277 command_terminals_are_test_commands(&stmt.command, source)
1278}
1279
1280pub(crate) fn command_terminals_are_test_commands(command: &Command, source: &str) -> bool {
1281 match command {
1282 Command::Simple(command) => matches!(
1283 static_word_text(&command.name, source).as_deref(),
1284 Some("[") | Some("test")
1285 ),
1286 Command::Compound(CompoundCommand::Conditional(_)) => true,
1287 Command::Binary(command) => logical_list_segments_are_test_commands(command, source),
1288 Command::Builtin(_)
1289 | Command::Decl(_)
1290 | Command::Compound(_)
1291 | Command::Function(_)
1292 | Command::AnonymousFunction(_) => false,
1293 }
1294}
1295
1296pub(crate) fn logical_list_segments_are_test_commands(
1297 command: &BinaryCommand,
1298 source: &str,
1299) -> bool {
1300 matches!(command.op, BinaryOp::And | BinaryOp::Or)
1301 && stmt_terminals_are_test_commands(&command.left, source)
1302 && stmt_terminals_are_test_commands(&command.right, source)
1303}
1304
1305pub(crate) fn collect_status_parameter_spans_in_stmt(
1306 stmt: &Stmt,
1307 source: &str,
1308 spans: &mut Vec<Span>,
1309) {
1310 collect_status_parameter_spans_in_command(&stmt.command, source, spans);
1311 for redirect in &stmt.redirects {
1312 if let Some(word) = redirect.word_target() {
1313 collect_status_parameter_spans_in_word(word, source, spans);
1314 }
1315 }
1316}
1317
1318pub(crate) fn collect_status_parameter_spans_in_return_guard_stmt(
1319 stmt: &Stmt,
1320 source: &str,
1321 spans: &mut Vec<Span>,
1322) {
1323 if let Command::Binary(command) = &stmt.command {
1324 collect_status_parameter_spans_in_stmt(&command.right, source, spans);
1325 return;
1326 }
1327
1328 collect_status_parameter_spans_in_stmt(stmt, source, spans);
1329}
1330
1331pub(crate) fn collect_unquoted_standalone_status_parameter_spans_in_stmt(
1332 stmt: &Stmt,
1333 source: &str,
1334 spans: &mut Vec<Span>,
1335) {
1336 collect_unquoted_standalone_status_parameter_spans_in_command(&stmt.command, source, spans);
1337 for redirect in &stmt.redirects {
1338 if let Some(word) = redirect.word_target() {
1339 collect_unquoted_standalone_status_parameter_spans_in_word(word, spans);
1340 }
1341 }
1342}
1343
1344pub(crate) fn collect_unquoted_standalone_status_parameter_spans_in_command(
1345 command: &Command,
1346 source: &str,
1347 spans: &mut Vec<Span>,
1348) {
1349 match command {
1350 Command::Simple(command) => {
1351 for assignment in &command.assignments {
1352 if let AssignmentValue::Scalar(word) = &assignment.value {
1353 collect_unquoted_standalone_status_parameter_spans_in_word(word, spans);
1354 }
1355 }
1356 collect_unquoted_standalone_status_parameter_spans_in_word(&command.name, spans);
1357 for word in &command.args {
1358 collect_unquoted_standalone_status_parameter_spans_in_word(word, spans);
1359 }
1360 }
1361 Command::Builtin(command) => match command {
1362 BuiltinCommand::Return(command) => {
1363 if let Some(word) = &command.code {
1364 collect_unquoted_standalone_status_parameter_spans_in_word(word, spans);
1365 }
1366 }
1367 BuiltinCommand::Exit(command) => {
1368 if let Some(word) = &command.code {
1369 collect_unquoted_standalone_status_parameter_spans_in_word(word, spans);
1370 }
1371 }
1372 BuiltinCommand::Break(_) | BuiltinCommand::Continue(_) => {}
1373 },
1374 Command::Decl(command) => {
1375 collect_unquoted_standalone_status_parameter_spans_in_assignments(
1376 &command.assignments,
1377 spans,
1378 );
1379 for operand in &command.operands {
1380 if let DeclOperand::Assignment(assignment) = operand {
1381 collect_unquoted_standalone_status_parameter_spans_in_assignment(
1382 assignment, spans,
1383 );
1384 }
1385 }
1386 }
1387 Command::Compound(command) => match command {
1388 CompoundCommand::Case(command) => {
1389 if let Some(first_stmt) = command.cases.iter().find_map(|case| case.body.first()) {
1390 collect_unquoted_standalone_status_parameter_spans_in_stmt(
1391 first_stmt, source, spans,
1392 );
1393 }
1394 }
1395 CompoundCommand::BraceGroup(body) | CompoundCommand::Subshell(body) => {
1396 if let Some(first_stmt) = body.first() {
1397 collect_unquoted_standalone_status_parameter_spans_in_stmt(
1398 first_stmt, source, spans,
1399 );
1400 }
1401 }
1402 CompoundCommand::Time(command) => {
1403 if let Some(inner) = &command.command {
1404 collect_unquoted_standalone_status_parameter_spans_in_stmt(
1405 inner, source, spans,
1406 );
1407 }
1408 }
1409 CompoundCommand::If(_)
1410 | CompoundCommand::While(_)
1411 | CompoundCommand::Until(_)
1412 | CompoundCommand::For(_)
1413 | CompoundCommand::Select(_)
1414 | CompoundCommand::Conditional(_)
1415 | CompoundCommand::Repeat(_)
1416 | CompoundCommand::Foreach(_)
1417 | CompoundCommand::ArithmeticFor(_)
1418 | CompoundCommand::Arithmetic(_)
1419 | CompoundCommand::Coproc(_)
1420 | CompoundCommand::Always(_) => {}
1421 },
1422 Command::Binary(command) => {
1423 collect_unquoted_standalone_status_parameter_spans_in_stmt(
1424 &command.left,
1425 source,
1426 spans,
1427 );
1428 }
1429 Command::Function(_) | Command::AnonymousFunction(_) => {}
1430 }
1431}
1432
1433pub(crate) fn collect_unquoted_standalone_status_parameter_spans_in_assignments(
1434 assignments: &[Assignment],
1435 spans: &mut Vec<Span>,
1436) {
1437 for assignment in assignments {
1438 collect_unquoted_standalone_status_parameter_spans_in_assignment(assignment, spans);
1439 }
1440}
1441
1442pub(crate) fn collect_unquoted_standalone_status_parameter_spans_in_assignment(
1443 assignment: &Assignment,
1444 spans: &mut Vec<Span>,
1445) {
1446 match &assignment.value {
1447 AssignmentValue::Scalar(word) => {
1448 collect_unquoted_standalone_status_parameter_spans_in_word(word, spans);
1449 }
1450 AssignmentValue::Compound(_) => {}
1451 }
1452}
1453
1454pub(crate) fn collect_unquoted_standalone_status_parameter_spans_in_word(
1455 word: &Word,
1456 spans: &mut Vec<Span>,
1457) {
1458 if word_is_unquoted_standalone_status_capture(word) {
1459 spans.push(word.span);
1460 }
1461}
1462
1463pub(crate) fn collect_status_parameter_spans_in_command(
1464 command: &Command,
1465 source: &str,
1466 spans: &mut Vec<Span>,
1467) {
1468 match command {
1469 Command::Simple(command) => {
1470 collect_status_parameter_spans_in_assignments(&command.assignments, source, spans);
1471 collect_status_parameter_spans_in_word(&command.name, source, spans);
1472 for word in &command.args {
1473 collect_status_parameter_spans_in_word(word, source, spans);
1474 }
1475 }
1476 Command::Builtin(command) => match command {
1477 BuiltinCommand::Break(command) => {
1478 collect_status_parameter_spans_in_assignments(&command.assignments, source, spans);
1479 if let Some(word) = &command.depth {
1480 collect_status_parameter_spans_in_word(word, source, spans);
1481 }
1482 for word in &command.extra_args {
1483 collect_status_parameter_spans_in_word(word, source, spans);
1484 }
1485 }
1486 BuiltinCommand::Continue(command) => {
1487 collect_status_parameter_spans_in_assignments(&command.assignments, source, spans);
1488 if let Some(word) = &command.depth {
1489 collect_status_parameter_spans_in_word(word, source, spans);
1490 }
1491 for word in &command.extra_args {
1492 collect_status_parameter_spans_in_word(word, source, spans);
1493 }
1494 }
1495 BuiltinCommand::Return(command) => {
1496 collect_status_parameter_spans_in_assignments(&command.assignments, source, spans);
1497 if let Some(word) = &command.code {
1498 collect_status_parameter_spans_in_word(word, source, spans);
1499 }
1500 for word in &command.extra_args {
1501 collect_status_parameter_spans_in_word(word, source, spans);
1502 }
1503 }
1504 BuiltinCommand::Exit(command) => {
1505 collect_status_parameter_spans_in_assignments(&command.assignments, source, spans);
1506 if let Some(word) = &command.code {
1507 collect_status_parameter_spans_in_word(word, source, spans);
1508 }
1509 for word in &command.extra_args {
1510 collect_status_parameter_spans_in_word(word, source, spans);
1511 }
1512 }
1513 },
1514 Command::Decl(command) => {
1515 collect_status_parameter_spans_in_assignments(&command.assignments, source, spans);
1516 for operand in &command.operands {
1517 match operand {
1518 DeclOperand::Flag(word) | DeclOperand::Dynamic(word) => {
1519 collect_status_parameter_spans_in_word(word, source, spans);
1520 }
1521 DeclOperand::Name(reference) => {
1522 collect_status_parameter_spans_in_var_ref(reference, source, spans);
1523 }
1524 DeclOperand::Assignment(assignment) => {
1525 collect_status_parameter_spans_in_assignment(assignment, source, spans);
1526 }
1527 }
1528 }
1529 }
1530 Command::Binary(command) => {
1531 collect_status_parameter_spans_in_stmt(&command.left, source, spans);
1532 }
1533 Command::Compound(command) => match command {
1534 CompoundCommand::If(command) => {
1535 if let Some(first_stmt) = command.condition.first() {
1536 collect_status_parameter_spans_in_stmt(first_stmt, source, spans);
1537 }
1538 }
1539 CompoundCommand::While(command) => {
1540 if let Some(first_stmt) = command.condition.first() {
1541 collect_status_parameter_spans_in_stmt(first_stmt, source, spans);
1542 }
1543 }
1544 CompoundCommand::Until(command) => {
1545 if let Some(first_stmt) = command.condition.first() {
1546 collect_status_parameter_spans_in_stmt(first_stmt, source, spans);
1547 }
1548 }
1549 CompoundCommand::Case(command) => {
1550 collect_status_parameter_spans_in_word(&command.word, source, spans);
1551 for case in &command.cases {
1552 if let Some(first_stmt) = case.body.first() {
1553 collect_status_parameter_spans_in_stmt(first_stmt, source, spans);
1554 }
1555 }
1556 }
1557 CompoundCommand::Subshell(body) | CompoundCommand::BraceGroup(body) => {
1558 if let Some(first_stmt) = body.first() {
1559 collect_status_parameter_spans_in_stmt(first_stmt, source, spans);
1560 }
1561 }
1562 CompoundCommand::Time(command) => {
1563 if let Some(command) = &command.command {
1564 collect_status_parameter_spans_in_stmt(command, source, spans);
1565 }
1566 }
1567 CompoundCommand::Conditional(command) => {
1568 collect_status_parameter_spans_in_conditional_expr(
1569 &command.expression,
1570 source,
1571 spans,
1572 );
1573 }
1574 CompoundCommand::Coproc(command) => {
1575 collect_status_parameter_spans_in_stmt(&command.body, source, spans);
1576 }
1577 CompoundCommand::Always(command) => {
1578 if let Some(first_stmt) = command.body.first() {
1579 collect_status_parameter_spans_in_stmt(first_stmt, source, spans);
1580 }
1581 }
1582 CompoundCommand::For(_)
1583 | CompoundCommand::Repeat(_)
1584 | CompoundCommand::Foreach(_)
1585 | CompoundCommand::ArithmeticFor(_)
1586 | CompoundCommand::Select(_)
1587 | CompoundCommand::Arithmetic(_) => {}
1588 },
1589 Command::Function(_) => {}
1590 Command::AnonymousFunction(command) => {
1591 collect_status_parameter_spans_in_stmt(&command.body, source, spans);
1592 for word in &command.args {
1593 collect_status_parameter_spans_in_word(word, source, spans);
1594 }
1595 }
1596 }
1597}
1598
1599pub(crate) fn collect_status_parameter_spans_in_assignments(
1600 assignments: &[Assignment],
1601 source: &str,
1602 spans: &mut Vec<Span>,
1603) {
1604 for assignment in assignments {
1605 collect_status_parameter_spans_in_assignment(assignment, source, spans);
1606 }
1607}
1608
1609pub(crate) fn collect_status_parameter_spans_in_assignment(
1610 assignment: &Assignment,
1611 source: &str,
1612 spans: &mut Vec<Span>,
1613) {
1614 collect_status_parameter_spans_in_var_ref(&assignment.target, source, spans);
1615 match &assignment.value {
1616 AssignmentValue::Scalar(word) => {
1617 collect_status_parameter_spans_in_word(word, source, spans)
1618 }
1619 AssignmentValue::Compound(array) => {
1620 for element in &array.elements {
1621 match element {
1622 ArrayElem::Sequential(word) => {
1623 collect_status_parameter_spans_in_word(word, source, spans);
1624 }
1625 ArrayElem::Keyed { key, value } | ArrayElem::KeyedAppend { key, value } => {
1626 visit_subscript_words(Some(key), source, &mut |word| {
1627 collect_status_parameter_spans_in_word(word, source, spans);
1628 });
1629 collect_status_parameter_spans_in_word(value, source, spans);
1630 }
1631 }
1632 }
1633 }
1634 }
1635}
1636
1637pub(crate) fn collect_status_parameter_spans_in_var_ref(
1638 reference: &VarRef,
1639 source: &str,
1640 spans: &mut Vec<Span>,
1641) {
1642 if reference.name.as_str() == "?" {
1643 spans.push(reference.span);
1644 }
1645
1646 visit_var_ref_subscript_words_with_source(reference, source, &mut |word| {
1647 collect_status_parameter_spans_in_word(word, source, spans);
1648 });
1649}
1650
1651pub(crate) fn collect_status_parameter_spans_in_word(
1652 word: &Word,
1653 source: &str,
1654 spans: &mut Vec<Span>,
1655) {
1656 for part in &word.parts {
1657 collect_status_parameter_spans_in_word_part(part, source, spans);
1658 }
1659}
1660
1661pub(crate) fn collect_status_parameter_spans_in_word_part(
1662 part: &WordPartNode,
1663 source: &str,
1664 spans: &mut Vec<Span>,
1665) {
1666 match &part.kind {
1667 WordPart::Literal(_) | WordPart::SingleQuoted { .. } | WordPart::ZshQualifiedGlob(_) => {}
1668 WordPart::DoubleQuoted { parts, .. } => {
1669 for nested_part in parts {
1670 collect_status_parameter_spans_in_word_part(nested_part, source, spans);
1671 }
1672 }
1673 WordPart::Variable(name) => {
1674 if name.as_str() == "?" {
1675 spans.push(part.span);
1676 }
1677 }
1678 WordPart::CommandSubstitution { body, .. } | WordPart::ProcessSubstitution { body, .. } => {
1679 if let Some(first_stmt) = body.first() {
1680 collect_status_parameter_spans_in_stmt(first_stmt, source, spans);
1681 }
1682 }
1683 WordPart::ArithmeticExpansion {
1684 expression_ast,
1685 expression_word_ast,
1686 ..
1687 } => {
1688 if let Some(expression) = expression_ast {
1689 visit_arithmetic_words(expression, &mut |word| {
1690 collect_status_parameter_spans_in_word(word, source, spans);
1691 });
1692 } else {
1693 collect_status_parameter_spans_in_word(expression_word_ast, source, spans);
1694 }
1695 }
1696 WordPart::Parameter(parameter) => {
1697 collect_status_parameter_spans_in_parameter_expansion(parameter, source, spans);
1698 }
1699 WordPart::ParameterExpansion {
1700 reference,
1701 operand,
1702 operand_word_ast,
1703 ..
1704 }
1705 | WordPart::IndirectExpansion {
1706 reference,
1707 operand,
1708 operand_word_ast,
1709 ..
1710 } => {
1711 if reference.name.as_str() == "?" {
1712 spans.push(part.span);
1713 }
1714 collect_status_parameter_spans_in_var_ref(reference, source, spans);
1715 collect_status_parameter_spans_in_fragment(
1716 operand_word_ast.as_deref(),
1717 operand.as_ref(),
1718 source,
1719 spans,
1720 );
1721 }
1722 WordPart::Length(reference)
1723 | WordPart::ArrayAccess(reference)
1724 | WordPart::ArrayLength(reference)
1725 | WordPart::ArrayIndices(reference)
1726 | WordPart::Transformation { reference, .. } => {
1727 if reference.name.as_str() == "?" {
1728 spans.push(part.span);
1729 }
1730 collect_status_parameter_spans_in_var_ref(reference, source, spans);
1731 }
1732 WordPart::Substring {
1733 reference,
1734 offset_ast,
1735 offset_word_ast,
1736 length_ast,
1737 length_word_ast,
1738 ..
1739 }
1740 | WordPart::ArraySlice {
1741 reference,
1742 offset_ast,
1743 offset_word_ast,
1744 length_ast,
1745 length_word_ast,
1746 ..
1747 } => {
1748 if reference.name.as_str() == "?" {
1749 spans.push(part.span);
1750 }
1751 collect_status_parameter_spans_in_var_ref(reference, source, spans);
1752 if let Some(offset_ast) = offset_ast {
1753 visit_arithmetic_words(offset_ast, &mut |word| {
1754 collect_status_parameter_spans_in_word(word, source, spans);
1755 });
1756 } else {
1757 collect_status_parameter_spans_in_word(offset_word_ast, source, spans);
1758 }
1759 match (length_ast.as_ref(), length_word_ast.as_ref()) {
1760 (Some(length_ast), _) => {
1761 visit_arithmetic_words(length_ast, &mut |word| {
1762 collect_status_parameter_spans_in_word(word, source, spans);
1763 });
1764 }
1765 (None, Some(length_word_ast)) => {
1766 collect_status_parameter_spans_in_word(length_word_ast, source, spans);
1767 }
1768 (None, None) => {}
1769 }
1770 }
1771 WordPart::PrefixMatch { .. } => {}
1772 }
1773}
1774
1775pub(crate) fn collect_status_parameter_spans_in_parameter_expansion(
1776 parameter: &shuck_ast::ParameterExpansion,
1777 source: &str,
1778 spans: &mut Vec<Span>,
1779) {
1780 match ¶meter.syntax {
1781 ParameterExpansionSyntax::Bourne(syntax) => match syntax {
1782 BourneParameterExpansion::Access { reference }
1783 | BourneParameterExpansion::Length { reference }
1784 | BourneParameterExpansion::Indices { reference }
1785 | BourneParameterExpansion::Transformation { reference, .. } => {
1786 collect_status_parameter_spans_in_var_ref(reference, source, spans);
1787 }
1788 BourneParameterExpansion::Indirect {
1789 reference,
1790 operand,
1791 operand_word_ast,
1792 ..
1793 }
1794 | BourneParameterExpansion::Operation {
1795 reference,
1796 operand,
1797 operand_word_ast,
1798 ..
1799 } => {
1800 collect_status_parameter_spans_in_var_ref(reference, source, spans);
1801 collect_status_parameter_spans_in_fragment(
1802 operand_word_ast.as_deref(),
1803 operand.as_ref(),
1804 source,
1805 spans,
1806 );
1807 }
1808 BourneParameterExpansion::Slice {
1809 reference,
1810 offset_ast,
1811 offset_word_ast,
1812 length_ast,
1813 length_word_ast,
1814 ..
1815 } => {
1816 collect_status_parameter_spans_in_var_ref(reference, source, spans);
1817 if let Some(offset_ast) = offset_ast {
1818 visit_arithmetic_words(offset_ast, &mut |word| {
1819 collect_status_parameter_spans_in_word(word, source, spans);
1820 });
1821 } else {
1822 collect_status_parameter_spans_in_word(offset_word_ast, source, spans);
1823 }
1824
1825 match (length_ast.as_ref(), length_word_ast.as_ref()) {
1826 (Some(length_ast), _) => {
1827 visit_arithmetic_words(length_ast, &mut |word| {
1828 collect_status_parameter_spans_in_word(word, source, spans);
1829 });
1830 }
1831 (None, Some(length_word_ast)) => {
1832 collect_status_parameter_spans_in_word(length_word_ast, source, spans);
1833 }
1834 (None, None) => {}
1835 }
1836 }
1837 BourneParameterExpansion::PrefixMatch { .. } => {}
1838 },
1839 ParameterExpansionSyntax::Zsh(syntax) => {
1840 collect_status_parameter_spans_in_zsh_target(&syntax.target, source, spans);
1841
1842 if let Some(operation) = &syntax.operation {
1843 match operation {
1844 shuck_ast::ZshExpansionOperation::PatternOperation { operand, .. }
1845 | shuck_ast::ZshExpansionOperation::Defaulting { operand, .. }
1846 | shuck_ast::ZshExpansionOperation::TrimOperation { operand, .. } => {
1847 collect_status_parameter_spans_in_fragment(
1848 operation.operand_word_ast(),
1849 Some(operand),
1850 source,
1851 spans,
1852 );
1853 }
1854 shuck_ast::ZshExpansionOperation::ReplacementOperation {
1855 pattern,
1856 replacement,
1857 ..
1858 } => {
1859 collect_status_parameter_spans_in_fragment(
1860 operation.pattern_word_ast(),
1861 Some(pattern),
1862 source,
1863 spans,
1864 );
1865 collect_status_parameter_spans_in_fragment(
1866 operation.replacement_word_ast(),
1867 replacement.as_ref(),
1868 source,
1869 spans,
1870 );
1871 }
1872 shuck_ast::ZshExpansionOperation::Slice { offset, length, .. } => {
1873 collect_status_parameter_spans_in_fragment(
1874 operation.offset_word_ast(),
1875 Some(offset),
1876 source,
1877 spans,
1878 );
1879 collect_status_parameter_spans_in_fragment(
1880 operation.length_word_ast(),
1881 length.as_ref(),
1882 source,
1883 spans,
1884 );
1885 }
1886 shuck_ast::ZshExpansionOperation::Unknown { text, .. } => {
1887 collect_status_parameter_spans_in_fragment(
1888 operation.operand_word_ast(),
1889 Some(text),
1890 source,
1891 spans,
1892 );
1893 }
1894 }
1895 }
1896 }
1897 }
1898}
1899
1900pub(crate) fn collect_status_parameter_spans_in_zsh_target(
1901 target: &ZshExpansionTarget,
1902 source: &str,
1903 spans: &mut Vec<Span>,
1904) {
1905 match target {
1906 ZshExpansionTarget::Reference(reference) => {
1907 collect_status_parameter_spans_in_var_ref(reference, source, spans);
1908 }
1909 ZshExpansionTarget::Nested(parameter) => {
1910 collect_status_parameter_spans_in_parameter_expansion(parameter, source, spans);
1911 }
1912 ZshExpansionTarget::Word(word) => {
1913 collect_status_parameter_spans_in_word(word, source, spans);
1914 }
1915 ZshExpansionTarget::Empty => {}
1916 }
1917}
1918
1919pub(crate) fn collect_status_parameter_spans_in_conditional_expr(
1920 expression: &ConditionalExpr,
1921 source: &str,
1922 spans: &mut Vec<Span>,
1923) {
1924 match expression {
1925 ConditionalExpr::Binary(expression) => {
1926 collect_status_parameter_spans_in_conditional_expr(&expression.left, source, spans);
1927 collect_status_parameter_spans_in_conditional_expr(&expression.right, source, spans);
1928 }
1929 ConditionalExpr::Unary(expression) => {
1930 collect_status_parameter_spans_in_conditional_expr(&expression.expr, source, spans);
1931 }
1932 ConditionalExpr::Parenthesized(expression) => {
1933 collect_status_parameter_spans_in_conditional_expr(&expression.expr, source, spans);
1934 }
1935 ConditionalExpr::Word(word) | ConditionalExpr::Regex(word) => {
1936 collect_status_parameter_spans_in_word(word, source, spans);
1937 }
1938 ConditionalExpr::Pattern(pattern) => {
1939 for part in &pattern.parts {
1940 if let PatternPart::Word(word) = &part.kind {
1941 collect_status_parameter_spans_in_word(word, source, spans);
1942 }
1943 }
1944 }
1945 ConditionalExpr::VarRef(reference) => {
1946 collect_status_parameter_spans_in_var_ref(reference, source, spans);
1947 }
1948 }
1949}
1950
1951pub(crate) fn collect_status_parameter_spans_in_fragment(
1952 word: Option<&Word>,
1953 text: Option<&SourceText>,
1954 source: &str,
1955 spans: &mut Vec<Span>,
1956) {
1957 let Some(text) = text else {
1958 return;
1959 };
1960 let snippet = text.slice(source);
1961 if !snippet.contains("$?") {
1962 return;
1963 }
1964 debug_assert!(
1965 word.is_some(),
1966 "parser-backed fragment text should always carry a word AST"
1967 );
1968 let Some(word) = word else {
1969 return;
1970 };
1971 collect_status_parameter_spans_in_word(word, source, spans);
1972}
1973
1974pub(crate) fn build_conditional_fact<'a>(
1975 expression_visits: &[ConditionalExpressionVisit<'a>],
1976 source: &str,
1977) -> Option<ConditionalFact<'a>> {
1978 if expression_visits.is_empty() {
1979 return None;
1980 }
1981
1982 let mut nodes = Vec::with_capacity(expression_visits.len());
1983 let mut mixed_logical_operators = Vec::new();
1984 for visit in expression_visits {
1985 nodes.push(build_conditional_node(visit.expression, source));
1986 collect_mixed_logical_operator(
1987 visit.expression,
1988 visit.parent_in_same_logical_group,
1989 &mut mixed_logical_operators,
1990 );
1991 }
1992
1993 (!nodes.is_empty()).then_some(ConditionalFact {
1994 nodes: nodes.into_boxed_slice(),
1995 mixed_logical_operators: mixed_logical_operators.into_boxed_slice(),
1996 })
1997}
1998
1999pub(crate) fn command_name_is_plain_command_substitution(word: &Word, source: &str) -> bool {
2000 let analysis = analyze_word(word, source, None);
2001 analysis.substitution_shape == WordSubstitutionShape::Plain
2002 && analysis.quote == WordQuote::Unquoted
2003 && matches!(
2004 word.parts.as_slice(),
2005 [WordPartNode {
2006 kind: WordPart::CommandSubstitution {
2007 syntax: CommandSubstitutionSyntax::DollarParen,
2008 ..
2009 },
2010 ..
2011 }]
2012 )
2013}
2014
2015pub(crate) fn collect_mixed_logical_operator(
2016 expression: &ConditionalExpr,
2017 parent_in_same_logical_group: bool,
2018 operators: &mut Vec<ConditionalMixedLogicalOperatorFact>,
2019) {
2020 let ConditionalExpr::Binary(binary) = expression else {
2021 return;
2022 };
2023
2024 if conditional_binary_op_is_logical(binary.op)
2025 && !parent_in_same_logical_group
2026 && logical_operator_mask(expression) == (LOGICAL_AND_MASK | LOGICAL_OR_MASK)
2027 {
2028 let grouped_subexpression_spans =
2029 mixed_logical_grouped_subexpression_spans(expression, binary.op);
2030 debug_assert!(
2031 !grouped_subexpression_spans.is_empty(),
2032 "mixed logical operators should expose at least one grouping span"
2033 );
2034 operators.push(ConditionalMixedLogicalOperatorFact {
2035 operator_span: binary.op_span,
2036 grouped_subexpression_spans: grouped_subexpression_spans.into_boxed_slice(),
2037 });
2038 }
2039}
2040
2041pub(crate) const LOGICAL_AND_MASK: u8 = 0b01;
2042pub(crate) const LOGICAL_OR_MASK: u8 = 0b10;
2043
2044pub(crate) fn mixed_logical_grouped_subexpression_spans(
2045 expression: &ConditionalExpr,
2046 group_op: ConditionalBinaryOp,
2047) -> Vec<Span> {
2048 let mut spans = Vec::new();
2049 collect_mixed_logical_grouped_subexpression_spans(expression, group_op, &mut spans);
2050 spans
2051}
2052
2053pub(crate) fn collect_mixed_logical_grouped_subexpression_spans(
2054 expression: &ConditionalExpr,
2055 group_op: ConditionalBinaryOp,
2056 spans: &mut Vec<Span>,
2057) {
2058 let ConditionalExpr::Binary(binary) = expression else {
2059 return;
2060 };
2061 if !conditional_binary_op_is_logical(binary.op) {
2062 return;
2063 }
2064
2065 if binary.op != group_op {
2066 spans.push(expression.span());
2067 return;
2068 }
2069
2070 collect_mixed_logical_grouped_subexpression_spans(&binary.left, group_op, spans);
2071 collect_mixed_logical_grouped_subexpression_spans(&binary.right, group_op, spans);
2072}
2073
2074pub(crate) fn logical_operator_mask(expression: &ConditionalExpr) -> u8 {
2075 match expression {
2076 ConditionalExpr::Parenthesized(_) => 0,
2077 ConditionalExpr::Unary(unary) => logical_operator_mask(&unary.expr),
2078 ConditionalExpr::Binary(binary) => {
2079 let own = match binary.op {
2080 ConditionalBinaryOp::And => LOGICAL_AND_MASK,
2081 ConditionalBinaryOp::Or => LOGICAL_OR_MASK,
2082 _ => 0,
2083 };
2084
2085 own | logical_operator_mask(&binary.left) | logical_operator_mask(&binary.right)
2086 }
2087 ConditionalExpr::Word(_)
2088 | ConditionalExpr::Pattern(_)
2089 | ConditionalExpr::Regex(_)
2090 | ConditionalExpr::VarRef(_) => 0,
2091 }
2092}
2093
2094pub(crate) fn conditional_binary_op_is_logical(operator: ConditionalBinaryOp) -> bool {
2095 matches!(operator, ConditionalBinaryOp::And | ConditionalBinaryOp::Or)
2096}
2097
2098pub(crate) fn build_conditional_node<'a>(
2099 expression: &'a ConditionalExpr,
2100 source: &str,
2101) -> ConditionalNodeFact<'a> {
2102 match expression {
2103 ConditionalExpr::Word(_) => ConditionalNodeFact::BareWord(ConditionalBareWordFact {
2104 expression,
2105 operand: build_conditional_operand_fact(expression, source),
2106 }),
2107 ConditionalExpr::Unary(unary) => ConditionalNodeFact::Unary(ConditionalUnaryFact {
2108 expression,
2109 op: unary.op,
2110 operator_family: conditional_unary_operator_family(unary.op),
2111 operand: build_conditional_operand_fact(&unary.expr, source),
2112 }),
2113 ConditionalExpr::Binary(binary) => ConditionalNodeFact::Binary(ConditionalBinaryFact {
2114 expression,
2115 op: binary.op,
2116 operator_family: conditional_binary_operator_family(binary.op),
2117 left: build_conditional_operand_fact(&binary.left, source),
2118 right: build_conditional_operand_fact(&binary.right, source),
2119 }),
2120 ConditionalExpr::Parenthesized(_)
2121 | ConditionalExpr::Pattern(_)
2122 | ConditionalExpr::Regex(_)
2123 | ConditionalExpr::VarRef(_) => ConditionalNodeFact::Other(expression),
2124 }
2125}
2126
2127pub(crate) fn build_conditional_operand_fact<'a>(
2128 expression: &'a ConditionalExpr,
2129 source: &str,
2130) -> ConditionalOperandFact<'a> {
2131 let expression = strip_parenthesized_conditionals(expression);
2132 let word = match expression {
2133 ConditionalExpr::Word(word) | ConditionalExpr::Regex(word) => Some(word),
2134 ConditionalExpr::Pattern(pattern) => conditional_pattern_single_word(pattern),
2135 ConditionalExpr::Binary(_)
2136 | ConditionalExpr::Unary(_)
2137 | ConditionalExpr::Parenthesized(_)
2138 | ConditionalExpr::VarRef(_) => None,
2139 };
2140
2141 ConditionalOperandFact {
2142 expression,
2143 class: classify_conditional_operand(expression, source),
2144 word,
2145 word_classification: word.map(|word| classify_word(word, source)),
2146 }
2147}
2148
2149pub(crate) fn conditional_pattern_single_word(pattern: &Pattern) -> Option<&Word> {
2150 match pattern.parts.as_slice() {
2151 [part] => match &part.kind {
2152 PatternPart::Word(word) => Some(word),
2153 PatternPart::Literal(_)
2154 | PatternPart::AnyString
2155 | PatternPart::AnyChar
2156 | PatternPart::CharClass(_)
2157 | PatternPart::Group { .. } => None,
2158 },
2159 _ => None,
2160 }
2161}
2162
2163pub(crate) fn strip_parenthesized_conditionals(
2164 mut expression: &ConditionalExpr,
2165) -> &ConditionalExpr {
2166 while let ConditionalExpr::Parenthesized(parenthesized) = expression {
2167 expression = &parenthesized.expr;
2168 }
2169
2170 expression
2171}
2172
2173pub(crate) fn conditional_unary_operator_family(
2174 operator: ConditionalUnaryOp,
2175) -> ConditionalOperatorFamily {
2176 if matches!(
2177 operator,
2178 ConditionalUnaryOp::EmptyString | ConditionalUnaryOp::NonEmptyString
2179 ) {
2180 ConditionalOperatorFamily::StringUnary
2181 } else {
2182 ConditionalOperatorFamily::Other
2183 }
2184}
2185
2186pub(crate) fn conditional_binary_operator_family(
2187 operator: ConditionalBinaryOp,
2188) -> ConditionalOperatorFamily {
2189 match operator {
2190 ConditionalBinaryOp::RegexMatch => ConditionalOperatorFamily::Regex,
2191 ConditionalBinaryOp::And | ConditionalBinaryOp::Or => ConditionalOperatorFamily::Logical,
2192 ConditionalBinaryOp::PatternEqShort
2193 | ConditionalBinaryOp::PatternEq
2194 | ConditionalBinaryOp::PatternNe
2195 | ConditionalBinaryOp::LexicalBefore
2196 | ConditionalBinaryOp::LexicalAfter => ConditionalOperatorFamily::StringBinary,
2197 ConditionalBinaryOp::NewerThan
2198 | ConditionalBinaryOp::OlderThan
2199 | ConditionalBinaryOp::SameFile
2200 | ConditionalBinaryOp::ArithmeticEq
2201 | ConditionalBinaryOp::ArithmeticNe
2202 | ConditionalBinaryOp::ArithmeticLe
2203 | ConditionalBinaryOp::ArithmeticGe
2204 | ConditionalBinaryOp::ArithmeticLt
2205 | ConditionalBinaryOp::ArithmeticGt => ConditionalOperatorFamily::Other,
2206 }
2207}