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
#[cfg(feature = "metacall-deploy")]
mod deploy_tests {
    use meta_ast::deploy::{DeployConfig, run_deploy};
    use meta_ast::output::OutputFormat;
    use std::fs;
    use std::path::PathBuf;
    use tempfile::tempdir;

    #[test]
    fn test_deploy_python_calls_js() {
        let root =
            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/mixed/python_calls_js");
        let out_dir = tempdir().unwrap();
        let out_path = out_dir.path().to_path_buf();

        let config = DeployConfig {
            root: root.clone(),
            out: out_path.clone(),
            format: OutputFormat::Json,
            check: false,
            max_pod_size: 20,
        };

        run_deploy(config).expect("Deploy failed");

        // Verify pod manifest was generated
        assert!(out_path.join("metacall.pods.json").exists());
        assert!(out_path.join("metacall.mesh.json").exists());

        // Verify pod manifest content
        let pod_content = fs::read_to_string(out_path.join("metacall.pods.json")).unwrap();
        let pod_json: serde_json::Value = serde_json::from_str(&pod_content).unwrap();
        assert_eq!(pod_json["version"], "1.1");
        assert!(!pod_json["deployments"].as_array().unwrap().is_empty());
        // python_calls_js has both Python and JS files
        let languages: Vec<&str> = pod_json["deployments"]
            .as_array()
            .unwrap()
            .iter()
            .filter_map(|d| d["language"].as_str())
            .collect();
        assert!(languages.contains(&"py"), "expected a Python pod");
        assert!(languages.contains(&"node"), "expected a Node pod");
        // Mesh annotation still uses the old filename
        let mesh_content = fs::read_to_string(out_path.join("metacall.mesh.json")).unwrap();
        let mesh_json: serde_json::Value = serde_json::from_str(&mesh_content).unwrap();
        assert_eq!(mesh_json["version"], "1.0");
        assert!(mesh_json["deployment_units"].as_array().unwrap().len() >= 2);
    }

    #[test]
    fn test_deploy_check_mode_pass() {
        let root =
            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/mixed/python_calls_js");

        let out_dir = tempdir().unwrap();
        let out_path = out_dir.path().to_path_buf();

        // Copy everything to temp dir
        copy_dir_recursive(&root, &out_path).unwrap();

        let config = DeployConfig {
            root: out_path.clone(),
            out: out_path.clone(),
            format: OutputFormat::Json,
            check: true,
            max_pod_size: 20,
        };

        // Without pre-existing cuts, the fairness check should pass
        let result = run_deploy(config);
        assert!(
            result.is_ok(),
            "check mode should pass without pre-existing metadata"
        );
    }

    #[test]
    fn max_pod_size_zero_returns_error() {
        let root = tempdir().unwrap();
        let config = DeployConfig {
            root: root.path().to_path_buf(),
            out: root.path().to_path_buf(),
            format: OutputFormat::Json,
            check: false,
            max_pod_size: 0,
        };
        assert!(run_deploy(config).is_err());
    }

    #[test]
    fn max_pod_size_forces_oversized_pod_cut() {
        let fixture = tempdir().unwrap();
        let root = fixture.path().join("proj");
        fs::create_dir_all(&root).unwrap();
        fs::write(root.join("a.py"), "import b\n").unwrap();
        fs::write(root.join("b.py"), "import a\n").unwrap();

        let run = |out_path: &std::path::Path, max_pod_size: usize| -> serde_json::Value {
            let config = DeployConfig {
                root: root.clone(),
                out: out_path.to_path_buf(),
                format: OutputFormat::Json,
                check: false,
                max_pod_size,
            };
            run_deploy(config).expect("Deploy failed");
            let content = fs::read_to_string(out_path.join("metacall.pods.json")).unwrap();
            serde_json::from_str(&content).unwrap()
        };

        let out_default = tempdir().unwrap();
        let default_manifest = run(
            out_default.path(),
            meta_ast::deploy::cut::DEFAULT_MAX_POD_SIZE,
        );
        let oversized_stubs: Vec<&serde_json::Value> = default_manifest["edges"]
            .as_array()
            .unwrap()
            .iter()
            .filter(|e| e["kind"].as_str() == Some("rpc_stub"))
            .filter(|e| {
                e["cut_annotations"].as_array().is_some_and(|cuts| {
                    cuts.iter()
                        .any(|cut| cut["cut_reason"].get("OversizedPod").is_some())
                })
            })
            .collect();
        assert!(
            oversized_stubs.is_empty(),
            "expected no OversizedPod rpc_stub edge at the default threshold, got {oversized_stubs:?}"
        );

        let out_small = tempdir().unwrap();
        let small_manifest = run(out_small.path(), 1);
        let oversized: Vec<&serde_json::Value> = small_manifest["edges"]
            .as_array()
            .unwrap()
            .iter()
            .filter(|e| e["kind"].as_str() == Some("rpc_stub"))
            .filter(|e| {
                e["cut_annotations"].as_array().is_some_and(|cuts| {
                    cuts.iter()
                        .any(|cut| cut["cut_reason"].get("OversizedPod").is_some())
                })
            })
            .collect();
        assert!(
            !oversized.is_empty(),
            "expected an OversizedPod rpc_stub edge at max_pod_size 1, edges: {}",
            small_manifest["edges"]
        );
    }

    #[test]
    fn config_path_resolves_scripts_relative_to_the_configuration() {
        let fixture = tempdir().unwrap();
        let proj = fixture.path();
        fs::create_dir_all(proj.join("cfg")).unwrap();
        fs::create_dir_all(proj.join("lib")).unwrap();
        fs::write(
            proj.join("caller.py"),
            "from metacall import metacall_load_from_configuration\nmetacall_load_from_configuration('cfg/deploy.json')\n",
        )
        .unwrap();
        fs::write(
            proj.join("cfg/deploy.json"),
            r#"{"language_id":"node","path":"../lib","scripts":["extra.js"]}"#,
        )
        .unwrap();
        fs::write(
            proj.join("lib/extra.js"),
            "function helper() { return 1; }\n",
        )
        .unwrap();

        let out = tempdir().unwrap();
        let config = DeployConfig {
            root: proj.to_path_buf(),
            out: out.path().to_path_buf(),
            format: OutputFormat::Json,
            check: false,
            max_pod_size: 20,
        };
        run_deploy(config).expect("Deploy failed");

        let manifest: serde_json::Value = serde_json::from_str(
            &fs::read_to_string(out.path().join("metacall.pods.json")).unwrap(),
        )
        .unwrap();
        let files: Vec<&str> = manifest["deployments"]
            .as_array()
            .unwrap()
            .iter()
            .flat_map(|d| d["files"].as_array().unwrap().iter())
            .filter_map(|f| f.as_str())
            .collect();
        assert!(
            files.iter().any(|f| f.ends_with("lib/extra.js")),
            "extra.js missing from pods: {files:?}"
        );
    }

    /// `-f yaml` must write the manifests in the requested format.
    #[test]
    fn deploy_yaml_format_writes_yaml_artifacts() {
        let root =
            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/mixed/python_calls_js");
        let out_dir = tempdir().unwrap();
        let out_path = out_dir.path().to_path_buf();

        let config = DeployConfig {
            root,
            out: out_path.clone(),
            format: OutputFormat::Yaml,
            check: false,
            max_pod_size: 20,
        };
        run_deploy(config).expect("Deploy failed");

        let pods = out_path.join("metacall.pods.yaml");
        let mesh = out_path.join("metacall.mesh.yaml");
        assert!(
            pods.exists(),
            "the pod manifest must be written in the requested format"
        );
        assert!(
            mesh.exists(),
            "the mesh annotation must be written in the requested format"
        );
        let pods_text = fs::read_to_string(&pods).unwrap();
        assert!(
            pods_text.contains("deployments"),
            "pod manifest content: {pods_text}"
        );
        let mesh_text = fs::read_to_string(&mesh).unwrap();
        assert!(
            mesh_text.contains("deployment_units"),
            "mesh content: {mesh_text}"
        );
    }

    /// An unmappable MetaCall load tag must be reported, not skipped silently.
    #[test]
    fn deploy_reports_an_unknown_load_tag() {
        let dir = tempdir().unwrap();
        fs::write(
            dir.path().join("app.py"),
            "from metacall import metacall_load_from_file\n\nmetacall_load_from_file(\"wasm\", [\"a.wasm\"])\n",
        )
        .unwrap();

        let output = std::process::Command::new(env!("CARGO_BIN_EXE_meta-ast"))
            .arg("deploy")
            .arg(dir.path())
            .arg("--out")
            .arg(dir.path().join("out"))
            .env("RUST_LOG", "warn")
            .output()
            .unwrap();

        let mut text = String::from_utf8_lossy(&output.stdout).to_string();
        text.push_str(&String::from_utf8_lossy(&output.stderr));
        assert!(
            text.contains("wasm"),
            "the unmappable tag must be named in a diagnostic, got:\n{text}"
        );
    }

    /// An in-memory load has no file: it must not become a node named by its code.
    #[test]
    fn memory_load_is_not_named_by_its_code() {
        let dir = tempdir().unwrap();
        fs::write(
            dir.path().join("package.json"),
            "{\"name\":\"probe\",\"version\":\"1.0.0\"}\n",
        )
        .unwrap();
        fs::write(
            dir.path().join("app.py"),
            "from metacall import metacall_load_from_memory\n\nmetacall_load_from_memory(\"node\", \"console.log(1)\")\n",
        )
        .unwrap();

        let out_dir = tempdir().unwrap();
        let config = DeployConfig {
            root: dir.path().to_path_buf(),
            out: out_dir.path().to_path_buf(),
            format: OutputFormat::Json,
            check: false,
            max_pod_size: 20,
        };
        run_deploy(config).expect("Deploy failed");

        let manifest: serde_json::Value = serde_json::from_str(
            &fs::read_to_string(out_dir.path().join("metacall.pods.json")).unwrap(),
        )
        .unwrap();
        let names: Vec<String> = manifest["deployments"]
            .as_array()
            .unwrap()
            .iter()
            .flat_map(|d| d["dependencies"].as_array().cloned().unwrap_or_default())
            .filter_map(|dep| dep["name"].as_str().map(str::to_string))
            .collect();
        assert!(
            !names.iter().any(|name| name.contains("console.log")),
            "a memory load must not name a node after its inline code: {names:?}"
        );
    }

    fn copy_dir_recursive(src: &std::path::Path, dst: &std::path::Path) -> std::io::Result<()> {
        fs::create_dir_all(dst)?;
        for entry in fs::read_dir(src)? {
            let entry = entry?;
            let file_type = entry.file_type()?;
            if file_type.is_dir() {
                copy_dir_recursive(&entry.path(), &dst.join(entry.file_name()))?;
            } else {
                fs::copy(entry.path(), dst.join(entry.file_name()))?;
            }
        }
        Ok(())
    }
}