1pub mod git;
17pub mod naming;
18pub mod path;
19pub mod visibility;
20
21use std::path::Path;
22
23use crate::ast::SymbolKind;
24use crate::git::GitRepository;
25
26use super::{Suggestion, SuggestionSource};
27
28pub struct HeuristicsEngine {
31 naming: naming::NamingHeuristics,
33
34 path: path::PathHeuristics,
36
37 #[allow(dead_code)]
39 visibility: visibility::VisibilityHeuristics,
40
41 git: git::GitHeuristics,
43
44 generate_summaries: bool,
46
47 use_git_heuristics: bool,
49}
50
51impl HeuristicsEngine {
52 pub fn new() -> Self {
54 Self {
55 naming: naming::NamingHeuristics::new(),
56 path: path::PathHeuristics::new(),
57 visibility: visibility::VisibilityHeuristics::new(),
58 git: git::GitHeuristics::new(),
59 generate_summaries: true,
60 use_git_heuristics: true,
61 }
62 }
63
64 pub fn with_summary_generation(mut self, enabled: bool) -> Self {
66 self.generate_summaries = enabled;
67 self
68 }
69
70 pub fn with_git_heuristics(mut self, enabled: bool) -> Self {
72 self.use_git_heuristics = enabled;
73 self
74 }
75
76 pub fn suggest(
84 &self,
85 target: &str,
86 line: usize,
87 symbol_kind: Option<SymbolKind>,
88 file_path: &str,
89 ) -> Vec<Suggestion> {
90 self.suggest_full(target, line, symbol_kind, file_path, None, false)
91 }
92
93 pub fn suggest_full(
97 &self,
98 target: &str,
99 line: usize,
100 symbol_kind: Option<SymbolKind>,
101 file_path: &str,
102 visibility: Option<crate::ast::Visibility>,
103 is_exported: bool,
104 ) -> Vec<Suggestion> {
105 let mut suggestions = Vec::new();
106
107 let is_file_level =
109 symbol_kind.is_none() && (target.contains('/') || target.contains('\\'));
110
111 if is_file_level {
112 if let Some(module_name) = self.path.infer_module_name(file_path) {
114 suggestions.push(
115 Suggestion::module(target, line, &module_name, SuggestionSource::Heuristic)
116 .with_confidence(0.7),
117 );
118
119 if self.generate_summaries {
121 let summary = format!("{} module", module_name);
122 suggestions.push(
123 Suggestion::summary(target, line, &summary, SuggestionSource::Heuristic)
124 .with_confidence(0.5),
125 );
126 }
127 }
128
129 return suggestions;
131 }
132
133 let naming_suggestions = self.naming.suggest(target, line);
135 suggestions.extend(naming_suggestions);
136
137 let path_suggestions = self.path.suggest(file_path, target, line);
139 suggestions.extend(path_suggestions);
140
141 if let Some(vis) = visibility {
143 let visibility_suggestions = self.visibility.suggest(target, line, vis, is_exported);
144 suggestions.extend(visibility_suggestions);
145 }
146
147 if self.generate_summaries {
149 if let Some(summary) = self.naming.generate_summary(target, symbol_kind) {
150 suggestions.push(
151 Suggestion::summary(target, line, summary, SuggestionSource::Heuristic)
152 .with_confidence(0.6),
153 );
154 }
155 }
156
157 suggestions
158 }
159
160 pub fn suggest_with_git(
167 &self,
168 target: &str,
169 line: usize,
170 symbol_kind: Option<SymbolKind>,
171 file_path: &str,
172 repo: Option<&GitRepository>,
173 ) -> Vec<Suggestion> {
174 self.suggest_with_git_full(target, line, symbol_kind, file_path, repo, None, false)
175 }
176
177 #[allow(clippy::too_many_arguments)]
185 pub fn suggest_with_git_full(
186 &self,
187 target: &str,
188 line: usize,
189 symbol_kind: Option<SymbolKind>,
190 file_path: &str,
191 repo: Option<&GitRepository>,
192 visibility: Option<crate::ast::Visibility>,
193 is_exported: bool,
194 ) -> Vec<Suggestion> {
195 let mut suggestions = self.suggest_full(
196 target,
197 line,
198 symbol_kind,
199 file_path,
200 visibility,
201 is_exported,
202 );
203
204 if self.use_git_heuristics {
206 if let Some(repo) = repo {
207 let path = Path::new(file_path);
208 let git_suggestions = self.git.suggest_for_file(repo, path, target, line);
209 suggestions.extend(git_suggestions);
210 }
211 }
212
213 suggestions
214 }
215}
216
217impl Default for HeuristicsEngine {
218 fn default() -> Self {
219 Self::new()
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226 use crate::annotate::AnnotationType;
227
228 #[test]
229 fn test_heuristics_engine_creation() {
230 let engine = HeuristicsEngine::new();
231 assert!(engine.generate_summaries);
232 }
233
234 #[test]
235 fn test_suggest_security_pattern() {
236 let engine = HeuristicsEngine::new();
237 let suggestions = engine.suggest(
238 "authenticateUser",
239 10,
240 Some(SymbolKind::Function),
241 "src/auth/service.ts",
242 );
243
244 let has_security = suggestions.iter().any(|s| {
246 s.annotation_type == AnnotationType::Domain && s.value.contains("security")
247 || s.annotation_type == AnnotationType::Lock
248 });
249
250 assert!(has_security || !suggestions.is_empty());
251 }
252
253 #[test]
254 fn test_suggest_from_path() {
255 let engine = HeuristicsEngine::new();
256 let suggestions = engine.suggest(
257 "processPayment",
258 10,
259 Some(SymbolKind::Function),
260 "src/billing/payments.ts",
261 );
262
263 let has_billing = suggestions
265 .iter()
266 .any(|s| s.annotation_type == AnnotationType::Domain && s.value == "billing");
267
268 assert!(has_billing);
269 }
270
271 #[test]
272 fn test_suggest_with_visibility_private() {
273 use crate::ast::Visibility;
274
275 let engine = HeuristicsEngine::new();
276 let suggestions = engine.suggest_full(
277 "internalHelper",
278 10,
279 Some(SymbolKind::Function),
280 "src/utils.ts",
281 Some(Visibility::Private),
282 false,
283 );
284
285 let has_restricted = suggestions
287 .iter()
288 .any(|s| s.annotation_type == AnnotationType::Lock && s.value == "restricted");
289
290 assert!(has_restricted, "Private symbols should get restricted lock");
291 }
292
293 #[test]
294 fn test_suggest_with_visibility_public_exported() {
295 use crate::ast::Visibility;
296
297 let engine = HeuristicsEngine::new();
298 let suggestions = engine.suggest_full(
299 "PublicAPI",
300 10,
301 Some(SymbolKind::Function),
302 "src/api.ts",
303 Some(Visibility::Public),
304 true, );
306
307 let has_normal = suggestions
309 .iter()
310 .any(|s| s.annotation_type == AnnotationType::Lock && s.value == "normal");
311
312 assert!(has_normal, "Public exported symbols should get normal lock");
313 }
314
315 #[test]
316 fn test_suggest_full_combines_all_sources() {
317 use crate::ast::Visibility;
318
319 let engine = HeuristicsEngine::new();
320 let suggestions = engine.suggest_full(
321 "authenticateUser",
322 10,
323 Some(SymbolKind::Function),
324 "src/auth/login.ts",
325 Some(Visibility::Internal),
326 false,
327 );
328
329 assert!(!suggestions.is_empty(), "Should have combined suggestions");
331
332 let has_naming = suggestions
334 .iter()
335 .any(|s| s.annotation_type == AnnotationType::Domain);
336 assert!(has_naming, "Should have naming/path-based suggestions");
337
338 let has_visibility = suggestions.iter().any(|s| {
340 s.annotation_type == AnnotationType::Lock || s.annotation_type == AnnotationType::AiHint
341 });
342 assert!(has_visibility, "Should have visibility-based suggestions");
343 }
344
345 #[test]
346 fn test_git_heuristics_disabled() {
347 let engine = HeuristicsEngine::new().with_git_heuristics(false);
348 assert!(!engine.use_git_heuristics);
349 }
350}