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};
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)
},
)
}
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();
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);
}
};
let hash = hasher::sha256(&content);
if !full {
if let Some(existing_hash) = db.get_file_hash(&rel_path)? {
if existing_hash == hash {
return Ok(FileOutcome::Skipped);
}
}
}
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);
let parse_result = parser.parse(&content, file_path);
let parse_status = if parse_result.has_errors {
ParseStatus::Partial
} else {
ParseStatus::Ok
};
let file_id = db.upsert_file(
scan_id,
&rel_path,
parser.language_name(),
size,
&hash,
modified,
parse_status,
)?;
db.delete_deps_for_file(file_id)?;
db.delete_symbols_for_file(file_id)?;
let (name_to_ids, symbols) = insert_symbols(db, file_id, &parse_result)?;
let same_file = same_file_deps::resolve_same_file(db, file_id, &parse_result, &name_to_ids)?;
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,
}))
}
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;
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))
}