bash-ast 0.8.20

Typed Rust AST over tree-sitter-bash. Parses bash source into a strongly-typed tree suitable for structural analysis (permission gating, linting, refactoring) rather than execution.
Documentation
//! Typed AST over tree-sitter-bash.
//!
//! tree-sitter gives back a stringly-typed CST (`node.kind() == "command"`); this crate
//! converts that into a Rust enum tree so downstream consumers (permission rules,
//! linters, refactors) can pattern-match instead of comparing strings.
//!
//! The typed surface mirrors `tree-sitter-bash`'s `node-types.json` one-to-one: every
//! named node kind has a corresponding variant. Conversion is total — an unrecognized
//! node kind returns [`ParseError::UnknownNode`] rather than being silently dropped.
pub mod ast;
pub mod summary;
pub mod tier;
pub mod tool_invocation;
pub mod wrappers;
mod convert;
mod error;
mod parse;

pub use error::ParseError;
pub use parse::{parse, parse_to_ast};

#[cfg(test)]
mod tests {
    use super::*;
    use summary::summarise_bash_shape;

    fn roundtrip_program(src: &str) {
        let program = parse_to_ast(src).expect("parse failed");
        let json = serde_json::to_string(&program).expect("serialize failed");
        let restored: ast::Program = serde_json::from_str(&json).expect("deserialize failed");
        assert_eq!(program, restored, "round-trip mismatch for: {src}");
    }

    fn roundtrip_shape(src: &str) {
        let program = parse_to_ast(src).expect("parse failed");
        let shape = summarise_bash_shape(&program);
        let json = serde_json::to_string(&shape).expect("serialize shape failed");
        let restored: summary::BashShape = serde_json::from_str(&json).expect("deserialize shape failed");
        assert_eq!(shape, restored, "shape round-trip mismatch for: {src}");
    }

    #[test]
    fn roundtrip_simple_command() {
        roundtrip_program("git push --force origin main");
    }

    #[test]
    fn roundtrip_pipeline() {
        roundtrip_program("head -n 20 server.log | tail -3");
    }

    #[test]
    fn roundtrip_wrapper_command() {
        roundtrip_program("timeout 30 git push --force origin main");
    }

    #[test]
    fn roundtrip_list_ops() {
        roundtrip_program("cd /tmp && cargo build --release || echo failed");
    }

    #[test]
    fn roundtrip_heredoc() {
        roundtrip_program("cat <<EOF\nhello world\nEOF");
    }

    #[test]
    fn shape_roundtrip_simple() {
        roundtrip_shape("git push origin main");
    }

    #[test]
    fn shape_roundtrip_pipeline() {
        roundtrip_shape("head -n 20 log | tail -3");
    }

    #[test]
    fn shape_roundtrip_wrapper() {
        roundtrip_shape("timeout 30 git push --force origin main");
    }

    #[test]
    fn shape_list_ops_are_strings() {
        let program = parse_to_ast("make && make install || echo failed").expect("parse");
        let shape = summarise_bash_shape(&program);
        assert!(shape.list_ops.iter().any(|op| op == "&&"), "missing &&: {:?}", shape.list_ops);
        assert!(shape.list_ops.iter().any(|op| op == "||"), "missing ||: {:?}", shape.list_ops);
        roundtrip_shape("make && make install || echo failed");
    }
}