1use crate::config::ResolvedConfig;
2use crate::env_value;
3use crate::llm::LLMClient;
4use crate::report_types::{LLMVerdict, StaticFlag};
5use crate::types::{FileRecord, MethodRecord, Reference};
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 callee_context: &[Reference],
50 on_progress: Option<&ReviewProgressCallback>,
51 ) -> Result<(Option<LLMVerdict>, usize, usize), String> {
52 method_review::analyze_method_review_with_context(
53 self,
54 method,
55 static_signals,
56 file_context,
57 callee_context,
58 on_progress,
59 )
60 .await
61 }
62
63 pub async fn analyze_file(
64 &self,
65 file: &FileRecord,
66 static_signals: &[String],
67 ) -> Result<(Option<LLMVerdict>, usize, usize), String> {
68 file_review::analyze_file(self, file, static_signals, None).await
69 }
70
71 async fn analyze_file_with_progress(
72 &self,
73 file: &FileRecord,
74 static_signals: &[String],
75 on_progress: Option<&ReviewProgressCallback>,
76 ) -> Result<(Option<LLMVerdict>, usize, usize), String> {
77 file_review::analyze_file(self, file, static_signals, on_progress).await
78 }
79}
80
81pub async fn analyze_with_client(
82 file_records: &[FileRecord],
83 static_flags: &[StaticFlag],
84 client: Arc<LLMClient>,
85 only_files: bool,
86 on_progress: Option<ReviewProgressCallback>,
87) -> Result<(Vec<LLMVerdict>, usize, usize), String> {
88 analyze_with_client_and_graph(
89 file_records,
90 static_flags,
91 client,
92 only_files,
93 on_progress,
94 None,
95 )
96 .await
97}
98
99pub async fn analyze_with_client_and_graph(
100 file_records: &[FileRecord],
101 static_flags: &[StaticFlag],
102 client: Arc<LLMClient>,
103 only_files: bool,
104 on_progress: Option<ReviewProgressCallback>,
105 graph: Option<&crate::symbol_graph::SymbolGraph>,
106) -> Result<(Vec<LLMVerdict>, usize, usize), String> {
107 analyze_with_client_and_graph_and_checkpoint(
108 file_records,
109 static_flags,
110 client,
111 only_files,
112 on_progress,
113 graph,
114 None,
115 )
116 .await
117}
118
119pub async fn analyze_with_client_and_graph_and_checkpoint(
120 file_records: &[FileRecord],
121 static_flags: &[StaticFlag],
122 client: Arc<LLMClient>,
123 only_files: bool,
124 on_progress: Option<ReviewProgressCallback>,
125 graph: Option<&crate::symbol_graph::SymbolGraph>,
126 checkpoint_path: Option<&std::path::Path>,
127) -> Result<(Vec<LLMVerdict>, usize, usize), String> {
128 let analyzer = Arc::new(Analyzer {
129 llm_client: client,
130 in_tok: AtomicUsize::new(0),
131 out_tok: AtomicUsize::new(0),
132 });
133
134 let (method_signals, file_signals) = signal_maps::build_static_signal_maps(static_flags);
135 let callee_contexts = graph
136 .map(|graph| crate::callgraph::build_callee_context(file_records, graph))
137 .unwrap_or_default();
138
139 let mut jobs = Vec::new();
140 let mut review_index = 0usize;
141
142 if !only_files {
143 for file in file_records {
144 for method in file
145 .methods
146 .iter()
147 .filter(|method| !support::is_rust_cfg_test_method(file, method))
148 {
149 let static_signals = method_signals
150 .get(&review_key(&method.file_path, &method.name))
151 .cloned()
152 .unwrap_or_default();
153 let callee_context = callee_contexts
154 .get(&(
155 method.file_path.clone(),
156 method.name.clone(),
157 method.start_line,
158 ))
159 .cloned()
160 .unwrap_or_default();
161 jobs.push(jobs::ReviewJob::Method {
162 index: review_index,
163 method: method.clone(),
164 static_signals,
165 file_context: support::surrounding_file_context(file, method),
166 callee_context,
167 });
168 review_index += 1;
169 }
170 }
171 }
172
173 for file in file_records.iter().cloned() {
174 let static_signals = file_signals
175 .get(&file.file_path)
176 .cloned()
177 .unwrap_or_default();
178 jobs.push(jobs::ReviewJob::File {
179 index: review_index,
180 file,
181 static_signals,
182 });
183 review_index += 1;
184 }
185
186 let review_context_key = analyzer.llm_client.review_context_key();
187 let verdicts = jobs::run_review_jobs(
188 Arc::clone(&analyzer),
189 jobs,
190 on_progress,
191 &review_context_key,
192 checkpoint_path,
193 )
194 .await?;
195
196 let total_in = analyzer.in_tok.load(Ordering::SeqCst);
197 let total_out = analyzer.out_tok.load(Ordering::SeqCst);
198 Ok((verdicts, total_in, total_out))
199}
200
201pub async fn analyze(
202 file_records: &[FileRecord],
203 static_flags: &[StaticFlag],
204 config: ResolvedConfig,
205 only_files: bool,
206 on_progress: Option<ReviewProgressCallback>,
207) -> Result<(Vec<LLMVerdict>, usize, usize), String> {
208 let api_key = env_value::read("SNIFF_API_KEY");
209 if api_key.is_none() {
210 if file_records.is_empty() {
211 return Ok((Vec::new(), 0, 0));
212 }
213 return Err(
214 "AI config is missing; set SNIFF_API_KEY and SNIFF_ENDPOINT before running Sniff."
215 .to_string(),
216 );
217 }
218
219 let client = LLMClient::try_new(config, api_key)
220 .map_err(|err| format!("LLM client initialization failed: {err}"))?;
221
222 analyze_with_client(
223 file_records,
224 static_flags,
225 Arc::new(client),
226 only_files,
227 on_progress,
228 )
229 .await
230}
231
232#[cfg(test)]
233#[path = "tests/analyzer.rs"]
234mod tests;