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};
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;