Skip to main content

bash_ast/
lib.rs

1//! Typed AST over tree-sitter-bash.
2//!
3//! tree-sitter gives back a stringly-typed CST (`node.kind() == "command"`); this crate
4//! converts that into a Rust enum tree so downstream consumers (permission rules,
5//! linters, refactors) can pattern-match instead of comparing strings.
6//!
7//! The typed surface mirrors `tree-sitter-bash`'s `node-types.json` one-to-one: every
8//! named node kind has a corresponding variant. Conversion is total — an unrecognized
9//! node kind returns [`ParseError::UnknownNode`] rather than being silently dropped.
10pub mod ast;
11pub mod summary;
12pub mod tier;
13pub mod tool_invocation;
14pub mod wrappers;
15mod convert;
16mod error;
17mod parse;
18
19pub use error::ParseError;
20pub use parse::{parse, parse_to_ast};
21
22#[cfg(test)]
23mod tests {
24    use super::*;
25    use summary::summarise_bash_shape;
26
27    fn roundtrip_program(src: &str) {
28        let program = parse_to_ast(src).expect("parse failed");
29        let json = serde_json::to_string(&program).expect("serialize failed");
30        let restored: ast::Program = serde_json::from_str(&json).expect("deserialize failed");
31        assert_eq!(program, restored, "round-trip mismatch for: {src}");
32    }
33
34    fn roundtrip_shape(src: &str) {
35        let program = parse_to_ast(src).expect("parse failed");
36        let shape = summarise_bash_shape(&program);
37        let json = serde_json::to_string(&shape).expect("serialize shape failed");
38        let restored: summary::BashShape = serde_json::from_str(&json).expect("deserialize shape failed");
39        assert_eq!(shape, restored, "shape round-trip mismatch for: {src}");
40    }
41
42    #[test]
43    fn roundtrip_simple_command() {
44        roundtrip_program("git push --force origin main");
45    }
46
47    #[test]
48    fn roundtrip_pipeline() {
49        roundtrip_program("head -n 20 server.log | tail -3");
50    }
51
52    #[test]
53    fn roundtrip_wrapper_command() {
54        roundtrip_program("timeout 30 git push --force origin main");
55    }
56
57    #[test]
58    fn roundtrip_list_ops() {
59        roundtrip_program("cd /tmp && cargo build --release || echo failed");
60    }
61
62    #[test]
63    fn roundtrip_heredoc() {
64        roundtrip_program("cat <<EOF\nhello world\nEOF");
65    }
66
67    #[test]
68    fn shape_roundtrip_simple() {
69        roundtrip_shape("git push origin main");
70    }
71
72    #[test]
73    fn shape_roundtrip_pipeline() {
74        roundtrip_shape("head -n 20 log | tail -3");
75    }
76
77    #[test]
78    fn shape_roundtrip_wrapper() {
79        roundtrip_shape("timeout 30 git push --force origin main");
80    }
81
82    #[test]
83    fn shape_list_ops_are_strings() {
84        let program = parse_to_ast("make && make install || echo failed").expect("parse");
85        let shape = summarise_bash_shape(&program);
86        assert!(shape.list_ops.iter().any(|op| op == "&&"), "missing &&: {:?}", shape.list_ops);
87        assert!(shape.list_ops.iter().any(|op| op == "||"), "missing ||: {:?}", shape.list_ops);
88        roundtrip_shape("make && make install || echo failed");
89    }
90}