meta-ast 0.7.0

Polyglot static-analysis engine: extract symbols and cross-language dependency graphs from 9 supported source languages, with optional MetaCall deployment manifest generation.
Documentation
use crate::model::output::InspectOutput;
use crate::model::{Symbol, SymbolKind};

/// Builds the inspect output by consuming owned `Symbol`s, moving their
/// fields into the bucketed output instead of cloning the immutable IR.
pub fn symbols_to_inspect_output(symbols: Vec<Symbol>) -> InspectOutput {
    let mut output = InspectOutput {
        funcs: Vec::new(),
        classes: Vec::new(),
        objects: Vec::new(),
    };

    for symbol in symbols {
        match symbol.kind {
            SymbolKind::Function | SymbolKind::Method | SymbolKind::Declaration => {
                output.funcs.push(crate::model::output::FuncEntry {
                    name: symbol.name,
                    source_range: symbol.source_range,
                    visibility: symbol.visibility,
                    signature: symbol.signature,
                    docstring: symbol.docstring,
                    is_async: symbol.is_async,
                });
            }
            SymbolKind::Class
            | SymbolKind::Struct
            | SymbolKind::Interface
            | SymbolKind::Trait
            | SymbolKind::Enum => {
                output.classes.push(crate::model::output::ClassEntry {
                    name: symbol.name,
                    source_range: symbol.source_range,
                    visibility: symbol.visibility,
                    signature: symbol.signature,
                    docstring: symbol.docstring,
                });
            }
            SymbolKind::Object
            | SymbolKind::Constant
            | SymbolKind::Static
            | SymbolKind::Module
            | SymbolKind::Namespace
            | SymbolKind::TypeAlias => {
                output.objects.push(crate::model::output::ObjectEntry {
                    name: symbol.name,
                    source_range: symbol.source_range,
                    visibility: symbol.visibility,
                    signature: symbol.signature,
                    docstring: symbol.docstring,
                });
            }
        }
    }

    output
}

pub fn serialize_inspect(
    symbols: &mut Vec<Symbol>,
    format: &crate::output::OutputFormat,
) -> anyhow::Result<String> {
    let output = symbols_to_inspect_output(std::mem::take(symbols));
    format.serialize(&output)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::language::LangId;
    use crate::model::{LineColumn, SourceRange, SymbolId, SymbolKind};
    use crate::output::OutputFormat;

    fn make_symbol(id: u32, name: &str, kind: SymbolKind) -> Symbol {
        Symbol {
            id: SymbolId::new(id).unwrap(),
            name: name.to_string(),
            kind,
            language: LangId::Python,
            file_path: std::path::PathBuf::from("test.py"),
            source_range: SourceRange {
                byte_start: 0,
                byte_end: 10,
                start: LineColumn { line: 0, column: 0 },
                end: LineColumn {
                    line: 0,
                    column: 10,
                },
            },
            name_range: None,
            visibility: None,
            signature: None,
            docstring: None,
            is_async: false,
        }
    }

    #[test]
    fn empty_symbols_produces_empty_json() {
        let json = serialize_inspect(&mut Vec::new(), &OutputFormat::Json).unwrap();
        assert_eq!(
            json,
            "{\n  \"funcs\": [],\n  \"classes\": [],\n  \"objects\": []\n}"
        );
    }

    #[test]
    fn symbols_map_to_expected_bucket() {
        let cases = [
            ("my_func", SymbolKind::Function, 1usize, 0usize, 0usize),
            ("do_thing", SymbolKind::Method, 1usize, 0usize, 0usize),
            ("MyClass", SymbolKind::Class, 0usize, 1usize, 0usize),
            ("Point", SymbolKind::Struct, 0usize, 1usize, 0usize),
            ("MY_CONST", SymbolKind::Constant, 0usize, 0usize, 1usize),
        ];

        for (name, kind, funcs, classes, objects) in cases {
            let sym = make_symbol(1, name, kind);
            let output = symbols_to_inspect_output(vec![sym]);
            assert_eq!(output.funcs.len(), funcs, "unexpected funcs for {name}");
            assert_eq!(
                output.classes.len(),
                classes,
                "unexpected classes for {name}"
            );
            assert_eq!(
                output.objects.len(),
                objects,
                "unexpected objects for {name}"
            );
            if funcs == 1 {
                assert_eq!(output.funcs[0].name, name);
            }
            if classes == 1 {
                assert_eq!(output.classes[0].name, name);
            }
            if objects == 1 {
                assert_eq!(output.objects[0].name, name);
            }
        }
    }

    #[test]
    fn to_inspect_json_valid() {
        let mut symbols = vec![
            make_symbol(1, "f1", SymbolKind::Function),
            make_symbol(2, "C1", SymbolKind::Class),
            make_symbol(3, "OBJ", SymbolKind::Constant),
        ];
        let json = serialize_inspect(&mut symbols, &OutputFormat::Json).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert!(parsed.is_object());
        assert!(parsed["funcs"].is_array());
        assert!(parsed["classes"].is_array());
        assert!(parsed["objects"].is_array());
    }

    #[test]
    fn inspect_output_required_keys() {
        let json = serialize_inspect(&mut Vec::new(), &OutputFormat::Json).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        let keys: std::collections::HashSet<&str> = parsed
            .as_object()
            .unwrap()
            .keys()
            .map(|s| s.as_str())
            .collect();
        let expected: std::collections::HashSet<&str> =
            ["funcs", "classes", "objects"].into_iter().collect();
        assert_eq!(keys, expected);
    }

    /// Visibility is a documented lowercase value in every output.
    #[test]
    fn visibility_is_lowercase_in_inspect_output() {
        let symbol = Symbol {
            id: crate::model::SymbolId::new(1).unwrap(),
            name: "encrypt".to_string(),
            kind: SymbolKind::Function,
            language: crate::language::LangId::Python,
            file_path: std::path::PathBuf::from("a.py"),
            source_range: crate::model::SourceRange {
                byte_start: 0,
                byte_end: 9,
                start: crate::model::LineColumn { line: 0, column: 0 },
                end: crate::model::LineColumn { line: 0, column: 9 },
            },
            name_range: None,
            visibility: Some(crate::model::Visibility::Public),
            signature: None,
            docstring: None,
            is_async: false,
        };

        let mut symbols = vec![symbol];
        let json = crate::output::inspect::serialize_inspect(
            &mut symbols,
            &crate::output::OutputFormat::Json,
        )
        .unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed["funcs"][0]["visibility"], "public");
    }
}