use crate::config::ResolvedConfig;
use crate::env_value;
use crate::llm::LLMClient;
use crate::report_types::{LLMVerdict, StaticFlag};
use crate::types::{FileRecord, MethodRecord};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use super::{ReviewProgress, ReviewProgressCallback};
#[path = "analyzer_prompts.rs"]
mod analyzer_prompts;
#[path = "analyzer_file.rs"]
mod file_review;
#[path = "analyzer_method.rs"]
mod method_review;
#[path = "analyzer_verdicts.rs"]
mod verdicts;
#[path = "analyzer_support.rs"]
mod support;
use support::review_key;
#[path = "analyzer_engine_jobs.rs"]
mod jobs;
#[path = "analyzer_signal_maps.rs"]
mod signal_maps;
pub struct Analyzer {
pub llm_client: Arc<LLMClient>,
pub in_tok: AtomicUsize,
pub out_tok: AtomicUsize,
}
impl Analyzer {
pub async fn analyze_method_review(
&self,
method: &MethodRecord,
static_signals: &[String],
) -> Result<(Option<LLMVerdict>, usize, usize), String> {
method_review::analyze_method_review(self, method, static_signals, None).await
}
async fn analyze_method_review_with_context(
&self,
method: &MethodRecord,
static_signals: &[String],
file_context: &str,
on_progress: Option<&ReviewProgressCallback>,
) -> Result<(Option<LLMVerdict>, usize, usize), String> {
method_review::analyze_method_review_with_context(
self,
method,
static_signals,
file_context,
on_progress,
)
.await
}
pub async fn analyze_file(
&self,
file: &FileRecord,
static_signals: &[String],
) -> Result<(Option<LLMVerdict>, usize, usize), String> {
file_review::analyze_file(self, file, static_signals, None).await
}
async fn analyze_file_with_progress(
&self,
file: &FileRecord,
static_signals: &[String],
on_progress: Option<&ReviewProgressCallback>,
) -> Result<(Option<LLMVerdict>, usize, usize), String> {
file_review::analyze_file(self, file, static_signals, on_progress).await
}
}
pub async fn analyze_with_client(
file_records: &[FileRecord],
static_flags: &[StaticFlag],
client: Arc<LLMClient>,
only_files: bool,
on_progress: Option<ReviewProgressCallback>,
) -> Result<(Vec<LLMVerdict>, usize, usize), String> {
let analyzer = Arc::new(Analyzer {
llm_client: client,
in_tok: AtomicUsize::new(0),
out_tok: AtomicUsize::new(0),
});
let (method_signals, file_signals) = signal_maps::build_static_signal_maps(static_flags);
let mut jobs = Vec::new();
let mut review_index = 0usize;
if !only_files {
for file in file_records {
for method in file
.methods
.iter()
.filter(|method| !support::is_rust_cfg_test_method(file, method))
{
let static_signals = method_signals
.get(&review_key(&method.file_path, &method.name))
.cloned()
.unwrap_or_default();
jobs.push(jobs::ReviewJob::Method {
index: review_index,
method: method.clone(),
static_signals,
file_context: support::surrounding_file_context(file, method),
});
review_index += 1;
}
}
}
for file in file_records.iter().cloned() {
let static_signals = file_signals
.get(&file.file_path)
.cloned()
.unwrap_or_default();
jobs.push(jobs::ReviewJob::File {
index: review_index,
file,
static_signals,
});
review_index += 1;
}
let verdicts = jobs::run_review_jobs(Arc::clone(&analyzer), jobs, on_progress).await?;
let total_in = analyzer.in_tok.load(Ordering::SeqCst);
let total_out = analyzer.out_tok.load(Ordering::SeqCst);
Ok((verdicts, total_in, total_out))
}
pub async fn analyze(
file_records: &[FileRecord],
static_flags: &[StaticFlag],
config: ResolvedConfig,
only_files: bool,
on_progress: Option<ReviewProgressCallback>,
) -> Result<(Vec<LLMVerdict>, usize, usize), String> {
let api_key = env_value::read("SNIFF_API_KEY");
if api_key.is_none() {
if file_records.is_empty() {
return Ok((Vec::new(), 0, 0));
}
return Err(
"AI config is missing; set SNIFF_API_KEY and SNIFF_ENDPOINT before running Sniff."
.to_string(),
);
}
let client = LLMClient::try_new(config, api_key)
.map_err(|err| format!("LLM client initialization failed: {err}"))?;
analyze_with_client(
file_records,
static_flags,
Arc::new(client),
only_files,
on_progress,
)
.await
}
#[cfg(test)]
#[path = "tests/analyzer.rs"]
mod tests;