astmap-core 0.0.2

Core domain types and logic for astmap
Documentation
use std::collections::HashMap;

use tracing::trace;

use crate::error::ScanError;
use crate::model::{DepLevel, DepType, ParseResult};
use crate::port::ScanStore;

use super::types::{SameFileResult, UnresolvedRef};

/// Classification of a single reference resolution attempt.
enum RefResolution {
    SameFile {
        source_id: i64,
        target_id: i64,
        dep_type: DepType,
    },
    CrossFile(UnresolvedRef),
}

/// Resolve same-file call and type_ref dependencies, collecting cross-file refs.
///
/// Separates classification (pure) from DB insertion (effect), then executes.
pub(super) fn resolve_same_file(
    db: &dyn ScanStore,
    file_id: i64,
    parse_result: &ParseResult,
    name_to_ids: &HashMap<String, Vec<i64>>,
) -> Result<SameFileResult, ScanError> {
    let resolve_name =
        |name: &str| -> Option<i64> { name_to_ids.get(name).and_then(|ids| ids.first().copied()) };

    // Phase 1: classify all references (pure — no side effects)
    let call_resolutions: Vec<RefResolution> = parse_result
        .calls
        .iter()
        .filter_map(|call| {
            let caller_id = resolve_name(&call.caller_name)?;
            trace!(
                "{} -> {} at line {}",
                call.caller_name,
                call.callee_name,
                call.line
            );
            Some(match resolve_name(&call.callee_name) {
                Some(callee_id) => RefResolution::SameFile {
                    source_id: caller_id,
                    target_id: callee_id,
                    dep_type: DepType::Calls,
                },
                None => RefResolution::CrossFile(UnresolvedRef {
                    source_file_id: file_id,
                    source_symbol_id: caller_id,
                    target_name: call.callee_name.clone(),
                    dep_type: DepType::Calls,
                }),
            })
        })
        .collect();

    let type_resolutions: Vec<RefResolution> = parse_result
        .type_refs
        .iter()
        .filter_map(|type_ref| {
            let from_id = resolve_name(&type_ref.from_symbol)?;
            trace!(
                "{} -> {} at line {}",
                type_ref.from_symbol,
                type_ref.to_type,
                type_ref.line
            );
            Some(match resolve_name(&type_ref.to_type) {
                Some(to_id) if from_id != to_id => RefResolution::SameFile {
                    source_id: from_id,
                    target_id: to_id,
                    dep_type: DepType::TypeRef,
                },
                Some(_) => return None, // self-reference, skip
                None => RefResolution::CrossFile(UnresolvedRef {
                    source_file_id: file_id,
                    source_symbol_id: from_id,
                    target_name: type_ref.to_type.clone(),
                    dep_type: DepType::TypeRef,
                }),
            })
        })
        .collect();

    // Phase 2: execute DB inserts and partition results
    let all_resolutions: Vec<RefResolution> = call_resolutions
        .into_iter()
        .chain(type_resolutions)
        .collect();

    let (dep_count, unresolved_refs) = all_resolutions.into_iter().try_fold(
        (0i64, Vec::<UnresolvedRef>::new()),
        |(count, mut unresolved), res| -> Result<_, ScanError> {
            match res {
                RefResolution::SameFile {
                    source_id,
                    target_id,
                    dep_type,
                } => {
                    db.insert_dependency(
                        file_id,
                        Some(source_id),
                        Some(file_id),
                        Some(target_id),
                        dep_type,
                        DepLevel::Symbol,
                        None,
                    )?;
                    Ok((count + 1, unresolved))
                }
                RefResolution::CrossFile(uref) => {
                    unresolved.push(uref);
                    Ok((count, unresolved))
                }
            }
        },
    )?;

    Ok(SameFileResult {
        dep_count,
        unresolved_refs,
    })
}