impactsense-parser 0.1.0

Multi-language static analysis: parse codebases into an in-memory dependency graph for impact analysis
Documentation
use std::path::{Path, PathBuf};

use thiserror::Error;

use crate::graph::{
    cleanup_incremental_targets_in_neo4j, persist_files_to_neo4j, GraphError, GraphPersistenceOptions,
    Neo4jConfig,
};
use crate::scanner::{scan_and_parse, FileScanConfig, ScannerError};
use crate::scanner_incremental::scan_and_parse_incremental_vector;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParsingMode {
    Bootstrap,
    IncrementalVector,
}

#[derive(Debug, Clone)]
pub struct ScanOptions {
    pub follow_symlinks: bool,
    pub max_file_size: Option<u64>,
    pub graph: GraphPersistenceOptions,
}

impl Default for ScanOptions {
    fn default() -> Self {
        Self {
            follow_symlinks: false,
            max_file_size: Some(2 * 1024 * 1024),
            graph: GraphPersistenceOptions::default(),
        }
    }
}

#[derive(Debug, Clone)]
pub struct PipelineReport {
    pub mode: ParsingMode,
    pub parsed_files: usize,
    pub cleanup_targets: usize,
}

#[derive(Debug, Error)]
pub enum PipelineError {
    #[error("scan/parse failed: {0}")]
    Scanner(#[from] ScannerError),
    #[error("neo4j persistence failed: {0}")]
    Graph(#[from] GraphError),
}

fn build_scan_config(root: &Path, options: &ScanOptions) -> FileScanConfig {
    let mut config = FileScanConfig::new(root);
    config.follow_symlinks = options.follow_symlinks;
    config.max_file_size = options.max_file_size;
    config
}

pub async fn run_bootstrap_pipeline(
    root: &Path,
    options: &ScanOptions,
    neo4j_cfg: Option<&Neo4jConfig>,
    clean: bool,
) -> Result<PipelineReport, PipelineError> {
    let config = build_scan_config(root, options);
    let parsed_files = scan_and_parse(&config)?;

    if let Some(cfg) = neo4j_cfg {
        persist_files_to_neo4j(
            cfg,
            root,
            &parsed_files,
            clean,
            options.follow_symlinks,
            &options.graph,
        )
        .await?;
    }

    Ok(PipelineReport {
        mode: ParsingMode::Bootstrap,
        parsed_files: parsed_files.len(),
        cleanup_targets: 0,
    })
}

pub async fn run_incremental_vector_pipeline(
    root: &Path,
    options: &ScanOptions,
    neo4j_cfg: Option<&Neo4jConfig>,
    parse_targets: &[String],
    cleanup_targets: &[String],
) -> Result<PipelineReport, PipelineError> {
    let config = build_scan_config(root, options);
    let parse_target_paths: Vec<PathBuf> = parse_targets.iter().map(PathBuf::from).collect();
    let parsed_files = scan_and_parse_incremental_vector(&config, &parse_target_paths)?;

    if let Some(cfg) = neo4j_cfg {
        cleanup_incremental_targets_in_neo4j(cfg, root, cleanup_targets).await?;
        persist_files_to_neo4j(
            cfg,
            root,
            &parsed_files,
            false,
            options.follow_symlinks,
            &options.graph,
        )
        .await?;
    }

    Ok(PipelineReport {
        mode: ParsingMode::IncrementalVector,
        parsed_files: parsed_files.len(),
        cleanup_targets: cleanup_targets.len(),
    })
}