Skip to main content

sniff/
analyzer_engine.rs

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;