astmap-core 0.0.1

Core domain types and logic for astmap
Documentation
use std::collections::HashMap;
use std::path::{Path, PathBuf};

use tracing::warn;

use crate::error::ScanError;
use crate::model::{ParseResult, ParseStatus};
use crate::port::{LanguageParser, LanguageRegistry, ScanStore};

use super::types::{FileOutcome, FileProcessResult, IndexingResult, PendingImport};
use super::{hasher, same_file_deps};

/// Parse all files, insert symbols, and collect imports and unresolved refs.
pub(super) fn index_files(
    project_dir: &Path,
    files: &[PathBuf],
    db: &dyn ScanStore,
    scan_id: i64,
    full: bool,
    lang: &dyn LanguageRegistry,
) -> Result<IndexingResult, ScanError> {
    files
        .iter()
        .filter_map(|file_path| {
            let ext = file_path.extension().and_then(|e| e.to_str()).unwrap_or("");
            let parser = lang.parser_for(ext)?;
            Some((file_path, ext, parser))
        })
        .try_fold(
            IndexingResult::default(),
            |mut acc, (file_path, ext, parser)| {
                match process_single_file(project_dir, file_path, ext, parser, db, scan_id, full)? {
                    FileOutcome::Skipped => acc.files_skipped += 1,
                    FileOutcome::Processed(r) => acc.merge(r),
                }
                Ok(acc)
            },
        )
}

/// Process a single source file: read, hash-check, parse, insert symbols, resolve same-file deps.
///
/// Returns `FileOutcome::Skipped` if the file was unreadable or unchanged (incremental),
/// or `FileOutcome::Processed` with all extracted data.
fn process_single_file(
    project_dir: &Path,
    file_path: &Path,
    ext: &str,
    parser: &dyn LanguageParser,
    db: &dyn ScanStore,
    scan_id: i64,
    full: bool,
) -> Result<FileOutcome, ScanError> {
    let rel_path = file_path
        .strip_prefix(project_dir)
        .unwrap_or(file_path)
        .to_string_lossy()
        .to_string();

    // Read file content
    let content = match std::fs::read_to_string(file_path) {
        Ok(c) => c,
        Err(e) => {
            warn!("cannot read {}: {}", rel_path, e);
            return Ok(FileOutcome::Skipped);
        }
    };

    // Compute hash for incremental scan
    let hash = hasher::sha256(&content);

    // Check if file changed (incremental)
    if !full {
        if let Some(existing_hash) = db.get_file_hash(&rel_path)? {
            if existing_hash == hash {
                return Ok(FileOutcome::Skipped);
            }
        }
    }

    // Get file metadata
    let metadata = std::fs::metadata(file_path)?;
    let size = metadata.len() as i64;
    let modified = metadata
        .modified()
        .map(|t| {
            t.duration_since(std::time::UNIX_EPOCH)
                .unwrap_or_default()
                .as_secs()
        })
        .unwrap_or(0);

    // Parse with tree-sitter
    let parse_result = parser.parse(&content, file_path);
    let parse_status = if parse_result.has_errors {
        ParseStatus::Partial
    } else {
        ParseStatus::Ok
    };

    // Upsert file
    let file_id = db.upsert_file(
        scan_id,
        &rel_path,
        parser.language_name(),
        size,
        &hash,
        modified,
        parse_status,
    )?;

    // Clear old deps and symbols for this file (deps first to avoid FK violations)
    db.delete_deps_for_file(file_id)?;
    db.delete_symbols_for_file(file_id)?;

    // Insert symbols and build name indexes
    let (name_to_ids, symbols) = insert_symbols(db, file_id, &parse_result)?;

    // Resolve same-file deps and collect cross-file unresolved refs
    let same_file = same_file_deps::resolve_same_file(db, file_id, &parse_result, &name_to_ids)?;

    // Collect imports for phase 2 resolution
    let file_import_names: Vec<((i64, String), String)> = parse_result
        .imports
        .iter()
        .flat_map(|imp| {
            imp.imported_symbols
                .iter()
                .map(move |name| ((file_id, name.clone()), imp.path.clone()))
        })
        .collect();

    let pending_imports: Vec<PendingImport> = parse_result
        .imports
        .iter()
        .map(|imp| PendingImport {
            source_file_id: file_id,
            source_rel_path: rel_path.clone(),
            source_extension: ext.to_string(),
            import_path: imp.path.clone(),
            imported_symbols: imp.imported_symbols.clone(),
        })
        .collect();

    Ok(FileOutcome::Processed(FileProcessResult {
        pending_imports,
        unresolved_refs: same_file.unresolved_refs,
        file_import_names,
        symbols,
        same_file_deps: same_file.dep_count,
        is_partial: parse_result.has_errors,
    }))
}

/// Insert symbols with parent-child hierarchy and build name lookup index.
///
/// Returns (name → all matching database IDs, symbol count).
fn insert_symbols(
    db: &dyn ScanStore,
    file_id: i64,
    parse_result: &ParseResult,
) -> Result<(HashMap<String, Vec<i64>>, i64), ScanError> {
    let index_to_db_id = parse_result.symbols.iter().enumerate().try_fold(
        HashMap::with_capacity(parse_result.symbols.len()),
        |mut acc, (idx, sym)| {
            let parent_db_id = sym
                .parent_index
                .and_then(|parent_idx| acc.get(&parent_idx).copied());

            let db_id = db.insert_symbol(
                file_id,
                parent_db_id,
                &sym.name,
                sym.kind,
                sym.signature.as_deref(),
                sym.start_line as i64,
                sym.end_line as i64,
                sym.start_byte as i64,
                sym.end_byte as i64,
            )?;
            acc.insert(idx, db_id);
            Ok::<_, ScanError>(acc)
        },
    )?;

    let symbol_count = index_to_db_id.len() as i64;

    // Build name → all matching db_ids
    let name_to_ids = parse_result
        .symbols
        .iter()
        .enumerate()
        .filter_map(|(idx, sym)| {
            index_to_db_id
                .get(&idx)
                .map(|&db_id| (sym.name.clone(), db_id))
        })
        .fold(
            HashMap::<String, Vec<i64>>::new(),
            |mut acc, (name, db_id)| {
                acc.entry(name).or_default().push(db_id);
                acc
            },
        );

    Ok((name_to_ids, symbol_count))
}