scryer-engine 0.2.1

Tree-sitter and stack-graphs AST indexing engine for Scryer code intelligence
use std::path::Path;

use scryer_engine::parsers::{ParseMode, RustAstParser};

#[test]
fn test_dependency_public_surface_filtering() {
    let code = r#"
//! Top-level module documentation for my_dep.

/// A public struct representing configuration.
pub struct MyConfig {
    pub port: u16,
    private_token: String,
}

struct InternalState {
    secret: u64,
}

pub trait Service {
    /// Dispatches a request.
    fn call(&self, req: &str) -> String;
}

trait SecretTrait {
    fn hidden(&self);
}

pub enum PublicStatus {
    Active,
    Disabled,
}

enum PrivateStatus {
    Unknown,
}

pub type PortNumber = u16;
type SecretType = String;

pub const DEFAULT_PORT: u16 = 8080;
const PRIVATE_SEED: u64 = 12345;

/// Public helper function.
pub fn public_helper(input: &str) -> usize {
    let mut intermediate_var = input.len();
    let closure = |x: usize| x * 2;
    closure(intermediate_var)
}

fn private_helper(input: &str) -> usize {
    input.len()
}

impl MyConfig {
    /// Creates a new config.
    pub fn new(port: u16) -> Self {
        let default_host = "localhost";
        let closure = || println!("{}", default_host);
        closure();
        Self {
            port,
            private_token: String::new(),
        }
    }

    fn internal_keygen(&self) -> String {
        format!("token-{}", self.port)
    }
}

impl InternalState {
    fn internal_step(&self) {}
}

pub mod nested {
    pub fn nested_public() {
        let scoped_x = 1;
    }

    fn nested_private() {}
}

mod private_mod {
    pub fn unreachable_fn() {}
}
"#;

    // 1. Parse in DependencyPublicSurface mode
    let dep_payload = RustAstParser::parse(
        Path::new("src/lib.rs"),
        code.as_bytes(),
        "hash_dep",
        ParseMode::DependencyPublicSurface {
            crate_name: "my_dep".to_string(),
        },
    )
    .expect("Dependency public surface parsing should succeed");

    assert_eq!(dep_payload.language, "rust");

    // Check symbols extracted
    let symbol_names: Vec<&str> = dep_payload
        .symbols
        .iter()
        .map(|s| s.name.as_str())
        .collect();

    // Public items MUST be extracted
    assert!(
        symbol_names.contains(&"MyConfig"),
        "pub struct MyConfig should exist"
    );
    assert!(
        symbol_names.contains(&"Service"),
        "pub trait Service should exist"
    );
    assert!(
        symbol_names.contains(&"PublicStatus"),
        "pub enum PublicStatus should exist"
    );
    assert!(
        symbol_names.contains(&"PortNumber"),
        "pub type PortNumber should exist"
    );
    assert!(
        symbol_names.contains(&"DEFAULT_PORT"),
        "pub const DEFAULT_PORT should exist"
    );
    assert!(
        symbol_names.contains(&"public_helper"),
        "pub fn public_helper should exist"
    );
    assert!(
        symbol_names.contains(&"new"),
        "pub fn new inside impl should exist"
    );
    assert!(
        symbol_names.contains(&"nested"),
        "pub mod nested should exist"
    );
    assert!(
        symbol_names.contains(&"nested_public"),
        "pub fn nested_public should exist"
    );

    // Private items MUST NOT be extracted
    assert!(
        !symbol_names.contains(&"InternalState"),
        "private struct must be omitted"
    );
    assert!(
        !symbol_names.contains(&"SecretTrait"),
        "private trait must be omitted"
    );
    assert!(
        !symbol_names.contains(&"PrivateStatus"),
        "private enum must be omitted"
    );
    assert!(
        !symbol_names.contains(&"SecretType"),
        "private type alias must be omitted"
    );
    assert!(
        !symbol_names.contains(&"PRIVATE_SEED"),
        "private const must be omitted"
    );
    assert!(
        !symbol_names.contains(&"private_helper"),
        "private fn must be omitted"
    );
    assert!(
        !symbol_names.contains(&"internal_keygen"),
        "private method in impl must be omitted"
    );
    assert!(
        !symbol_names.contains(&"internal_step"),
        "private method in impl must be omitted"
    );
    assert!(
        !symbol_names.contains(&"nested_private"),
        "private fn in nested mod must be omitted"
    );
    assert!(
        !symbol_names.contains(&"private_mod"),
        "private mod must be omitted"
    );
    assert!(
        !symbol_names.contains(&"unreachable_fn"),
        "fn in private mod must be omitted"
    );

    // Verify qualified names use crate root instead of `crate::`
    let config_sym = dep_payload
        .symbols
        .iter()
        .find(|s| s.name == "MyConfig")
        .unwrap();
    assert_eq!(config_sym.qualified_name, "my_dep::MyConfig");
    assert_eq!(config_sym.visibility, "public");
    assert!(
        config_sym
            .docstring
            .as_ref()
            .unwrap()
            .contains("A public struct representing configuration.")
    );

    let helper_sym = dep_payload
        .symbols
        .iter()
        .find(|s| s.name == "public_helper")
        .unwrap();
    assert_eq!(helper_sym.qualified_name, "my_dep::public_helper");
    assert!(helper_sym.signature.contains("pub fn public_helper"));
    assert!(
        helper_sym
            .docstring
            .as_ref()
            .unwrap()
            .contains("Public helper function.")
    );

    let method_sym = dep_payload
        .symbols
        .iter()
        .find(|s| s.name == "new")
        .unwrap();
    assert_eq!(method_sym.qualified_name, "my_dep::MyConfig::new");

    // In dependency public surface mode, function bodies are bypassed, so no function or closure scopes exist
    assert!(
        dep_payload.scopes.is_empty(),
        "Expected scopes to be empty in DependencyPublicSurface mode, got {}",
        dep_payload.scopes.len()
    );

    // 2. Parse in Workspace mode for comparison
    let ws_payload = RustAstParser::parse(
        Path::new("src/lib.rs"),
        code.as_bytes(),
        "hash_ws",
        ParseMode::Workspace,
    )
    .expect("Workspace parsing should succeed");

    // Verify that workspace mode contains private symbols and scopes
    let ws_symbol_names: Vec<&str> = ws_payload.symbols.iter().map(|s| s.name.as_str()).collect();
    assert!(ws_symbol_names.contains(&"private_helper"));
    assert!(ws_symbol_names.contains(&"InternalState"));
    assert!(
        ws_payload.scopes.len() > 5,
        "Workspace mode must extract lexical scopes"
    );

    // 3. Verify storage reduction: symbol count and entity footprints
    let dep_symbol_count = dep_payload.symbols.len();
    let ws_symbol_count = ws_payload.symbols.len();
    println!("Dependency symbols: {dep_symbol_count}, Workspace symbols: {ws_symbol_count}");

    // Total entities = symbols + scopes
    let dep_total_entities = dep_payload.symbols.len() + dep_payload.scopes.len();
    let ws_total_entities = ws_payload.symbols.len() + ws_payload.scopes.len();
    let reduction_pct = 100.0 * (1.0 - (dep_total_entities as f64 / ws_total_entities as f64));
    println!(
        "Entity reduction: {dep_total_entities} vs {ws_total_entities} ({reduction_pct:.1}% reduction)"
    );
    assert!(
        reduction_pct >= 60.0,
        "Expected >= 60% entity reduction on this sample (got {reduction_pct:.1}%)"
    );
}

#[test]
fn test_dependency_module_path_resolution() {
    let cases = [
        ("src/lib.rs", "tokio", "tokio"),
        ("src/sync/mod.rs", "tokio", "tokio::sync"),
        ("src/sync/mpsc.rs", "tokio", "tokio::sync::mpsc"),
        ("src/sync/mpsc/mod.rs", "tokio", "tokio::sync::mpsc"),
        ("lib.rs", "serde-json", "serde_json"),
        ("value/mod.rs", "serde-json", "serde_json::value"),
        ("value/ser.rs", "serde-json", "serde_json::value::ser"),
    ];

    for (rel_path, crate_name, expected) in cases {
        let computed =
            scryer_engine::parsers::dependency_module_path_for(Path::new(rel_path), crate_name);
        assert_eq!(
            computed, expected,
            "Failed for path={rel_path}, crate={crate_name}"
        );
    }
}