1use crate::config::ResolvedConfig;
2use crate::env_value;
3use crate::llm::LLMClient;
4use crate::report_types::{LLMVerdict, StaticFlag};
5use crate::types::{FileRecord, MethodRecord};
6use std::sync::Arc;
7use std::sync::atomic::{AtomicUsize, Ordering};
8
9use super::{ReviewProgress, ReviewProgressCallback};
10
11#[path = "analyzer_prompts.rs"]
12mod analyzer_prompts;
13#[path = "analyzer_file.rs"]
14mod file_review;
15#[path = "analyzer_method.rs"]
16mod method_review;
17#[path = "analyzer_verdicts.rs"]
18mod verdicts;
19
20#[path = "analyzer_support.rs"]
21mod support;
22use support::review_key;
23
24#[path = "analyzer_engine_jobs.rs"]
25mod jobs;
26#[path = "analyzer_signal_maps.rs"]
27mod signal_maps;
28
29pub struct Analyzer {
30 pub llm_client: Arc<LLMClient>,
31 pub in_tok: AtomicUsize,
32 pub out_tok: AtomicUsize,
33}
34
35impl Analyzer {
36 pub async fn analyze_method_review(
37 &self,
38 method: &MethodRecord,
39 static_signals: &[String],
40 ) -> Result<(Option<LLMVerdict>, usize, usize), String> {
41 method_review::analyze_method_review(self, method, static_signals, None).await
42 }
43
44 async fn analyze_method_review_with_context(
45 &self,
46 method: &MethodRecord,
47 static_signals: &[String],
48 file_context: &str,
49 on_progress: Option<&ReviewProgressCallback>,
50 ) -> Result<(Option<LLMVerdict>, usize, usize), String> {
51 method_review::analyze_method_review_with_context(
52 self,
53 method,
54 static_signals,
55 file_context,
56 on_progress,
57 )
58 .await
59 }
60
61 pub async fn analyze_file(
62 &self,
63 file: &FileRecord,
64 static_signals: &[String],
65 ) -> Result<(Option<LLMVerdict>, usize, usize), String> {
66 file_review::analyze_file(self, file, static_signals, None).await
67 }
68
69 async fn analyze_file_with_progress(
70 &self,
71 file: &FileRecord,
72 static_signals: &[String],
73 on_progress: Option<&ReviewProgressCallback>,
74 ) -> Result<(Option<LLMVerdict>, usize, usize), String> {
75 file_review::analyze_file(self, file, static_signals, on_progress).await
76 }
77}
78
79pub async fn analyze_with_client(
80 file_records: &[FileRecord],
81 static_flags: &[StaticFlag],
82 client: Arc<LLMClient>,
83 only_files: bool,
84 on_progress: Option<ReviewProgressCallback>,
85) -> Result<(Vec<LLMVerdict>, usize, usize), String> {
86 let analyzer = Arc::new(Analyzer {
87 llm_client: client,
88 in_tok: AtomicUsize::new(0),
89 out_tok: AtomicUsize::new(0),
90 });
91
92 let (method_signals, file_signals) = signal_maps::build_static_signal_maps(static_flags);
93
94 let mut jobs = Vec::new();
95 let mut review_index = 0usize;
96
97 if !only_files {
98 for file in file_records {
99 for method in file
100 .methods
101 .iter()
102 .filter(|method| !support::is_rust_cfg_test_method(file, method))
103 {
104 let static_signals = method_signals
105 .get(&review_key(&method.file_path, &method.name))
106 .cloned()
107 .unwrap_or_default();
108 jobs.push(jobs::ReviewJob::Method {
109 index: review_index,
110 method: method.clone(),
111 static_signals,
112 file_context: support::surrounding_file_context(file, method),
113 });
114 review_index += 1;
115 }
116 }
117 }
118
119 for file in file_records.iter().cloned() {
120 let static_signals = file_signals
121 .get(&file.file_path)
122 .cloned()
123 .unwrap_or_default();
124 jobs.push(jobs::ReviewJob::File {
125 index: review_index,
126 file,
127 static_signals,
128 });
129 review_index += 1;
130 }
131
132 let verdicts = jobs::run_review_jobs(Arc::clone(&analyzer), jobs, on_progress).await?;
133
134 let total_in = analyzer.in_tok.load(Ordering::SeqCst);
135 let total_out = analyzer.out_tok.load(Ordering::SeqCst);
136 Ok((verdicts, total_in, total_out))
137}
138
139pub async fn analyze(
140 file_records: &[FileRecord],
141 static_flags: &[StaticFlag],
142 config: ResolvedConfig,
143 only_files: bool,
144 on_progress: Option<ReviewProgressCallback>,
145) -> Result<(Vec<LLMVerdict>, usize, usize), String> {
146 let api_key = env_value::read("SNIFF_API_KEY");
147 if api_key.is_none() {
148 if file_records.is_empty() {
149 return Ok((Vec::new(), 0, 0));
150 }
151 return Err(
152 "AI config is missing; set SNIFF_API_KEY and SNIFF_ENDPOINT before running Sniff."
153 .to_string(),
154 );
155 }
156
157 let client = LLMClient::try_new(config, api_key)
158 .map_err(|err| format!("LLM client initialization failed: {err}"))?;
159
160 analyze_with_client(
161 file_records,
162 static_flags,
163 Arc::new(client),
164 only_files,
165 on_progress,
166 )
167 .await
168}
169
170#[cfg(test)]
171#[path = "tests/analyzer.rs"]
172mod tests;