Skip to main content

big_code_analysis_ast/lang_helpers/
bash.rs

1//! Bash: the logical connectives and the string comparison of a
2//! `[ … ]` / `[[ … ]]` test.
3
4use crate::Bash;
5use crate::node::Node;
6
7/// The `&&` / `||` a `[ … ]` connective stands for: `Some(AMPAMP)` for
8/// `-a` and `Some(PIPEPIPE)` for `-o`, when `op`, a child of `parent`,
9/// is one.
10///
11/// tree-sitter-bash gives every `-x` operator the one kind
12/// `test_operator`, so the text decides. Only a binary position joins
13/// two tests: the unary `-a FILE` (a file-exists test) has a
14/// `unary_expression` parent and is a comparison like `-f FILE`. Every
15/// expression kind is matched by name, not by `kind_id`, because the
16/// grammar aliases `binary_expression` five times (grammar-dispatch §1).
17///
18/// The `test` builtin spelling (`test "$a" = 1 -a "$b" = 2`) is out of
19/// reach: the grammar parses its arguments as plain `word`s, so neither
20/// its comparisons nor its connectives exist as nodes, and it scores as
21/// one command.
22#[must_use]
23pub fn bash_test_connective(op: &Node, parent: &Node, code: &[u8]) -> Option<Bash> {
24    if op.kind_id() != Bash::TestOperator as u16 || parent.kind() != "binary_expression" {
25        return None;
26    }
27    match code.get(op.start_byte()..op.end_byte())? {
28        b"-a" => Some(Bash::AMPAMP),
29        b"-o" => Some(Bash::PIPEPIPE),
30        _ => None,
31    }
32}
33
34/// How many `=` string comparisons `test`, a `[ … ]` / `[[ … ]]`
35/// `test_command`, holds.
36///
37/// Both spellings of `=` are a `binary_expression`: `[ "$a" = 1 ]`
38/// compares and `(( x = 1 ))` assigns. So an `=` compares exactly when
39/// the test reaches it through expression nodes alone. The descent
40/// passes through `binary_expression` because tree-sitter-bash nests `=`
41/// wrongly — `[ "$a" = 1 -a "$b" = 2 ]` parses as
42/// `"$a" = (1 -a "$b") = 2` — and through the unary and parenthesized
43/// forms (`[[ ! ( "$a" = 1 ) ]]`). It stops at anything else, so the
44/// assignment in `[[ $(( x = 1 )) -eq 1 ]]` is not counted.
45///
46/// Counted from the test down rather than by climbing from each `=` to
47/// its test: on the mis-nested spine a climb per `=` is `O(depth)`, and a
48/// long `[ … = … -o … ]` made the walk quadratic. The descent is an
49/// explicit stack, since that spine is as deep as the test is long.
50#[must_use]
51pub fn bash_test_eq_count(test: &Node) -> usize {
52    let mut count = 0;
53    let mut pending = vec![*test];
54    while let Some(expression) = pending.pop() {
55        for child in expression.children() {
56            if child.kind_id() == Bash::EQ as u16 {
57                count += 1;
58            } else if matches!(
59                child.kind(),
60                "binary_expression" | "unary_expression" | "parenthesized_expression"
61            ) {
62                pending.push(child);
63            }
64        }
65    }
66    count
67}