1use crate::types::Evidence;
7use std::collections::{HashMap, HashSet};
8
9#[derive(Debug, Clone, Default)]
11pub struct PatternSignals {
12 pub soft_delete: SoftDeleteSignals,
14 pub error_handling: ErrorHandlingSignals,
16 pub naming: NamingSignals,
18 pub resource_management: ResourceManagementSignals,
20 pub validation: ValidationSignals,
22 pub test_idioms: TestIdiomSignals,
24 pub import_patterns: ImportPatternSignals,
26 pub type_coverage: TypeCoverageSignals,
28 pub api_conventions: ApiConventionSignals,
30 pub async_patterns: AsyncPatternSignals,
32 pub extensions: HashMap<String, Vec<Evidence>>,
35}
36
37impl PatternSignals {
38 pub fn merge(&mut self, other: &PatternSignals) {
40 self.soft_delete.merge(&other.soft_delete);
41 self.error_handling.merge(&other.error_handling);
42 self.naming.merge(&other.naming);
43 self.resource_management.merge(&other.resource_management);
44 self.validation.merge(&other.validation);
45 self.test_idioms.merge(&other.test_idioms);
46 self.import_patterns.merge(&other.import_patterns);
47 self.type_coverage.merge(&other.type_coverage);
48 self.api_conventions.merge(&other.api_conventions);
49 self.async_patterns.merge(&other.async_patterns);
50 for (key, values) in &other.extensions {
51 self.extensions
52 .entry(key.clone())
53 .or_default()
54 .extend(values.clone());
55 }
56 }
57}
58
59#[derive(Debug, Clone, Default)]
65pub struct SoftDeleteSignals {
66 pub is_deleted_fields: Vec<Evidence>,
68 pub deleted_at_fields: Vec<Evidence>,
70 pub delete_query_filters: Vec<Evidence>,
72 pub paranoid_annotations: Vec<Evidence>,
74}
75
76impl SoftDeleteSignals {
77 pub fn merge(&mut self, other: &SoftDeleteSignals) {
79 self.is_deleted_fields
80 .extend(other.is_deleted_fields.clone());
81 self.deleted_at_fields
82 .extend(other.deleted_at_fields.clone());
83 self.delete_query_filters
84 .extend(other.delete_query_filters.clone());
85 self.paranoid_annotations
86 .extend(other.paranoid_annotations.clone());
87 }
88
89 pub fn has_signals(&self) -> bool {
91 !self.is_deleted_fields.is_empty()
92 || !self.deleted_at_fields.is_empty()
93 || !self.delete_query_filters.is_empty()
94 || !self.paranoid_annotations.is_empty()
95 }
96
97 pub fn calculate_confidence(&self) -> f64 {
99 let mut confidence: f64 = 0.0;
100 if !self.is_deleted_fields.is_empty() {
101 confidence += 0.4;
102 }
103 if !self.deleted_at_fields.is_empty() {
104 confidence += 0.4;
105 }
106 if !self.delete_query_filters.is_empty() {
107 confidence += 0.2;
108 }
109 if !self.paranoid_annotations.is_empty() {
110 confidence += 0.3;
111 }
112 confidence.min(1.0)
113 }
114}
115
116#[derive(Debug, Clone, Default)]
122pub struct ErrorHandlingSignals {
123 pub try_except_blocks: Vec<Evidence>,
125 pub custom_exceptions: Vec<(String, Evidence)>,
127 pub result_types: Vec<Evidence>,
129 pub question_mark_ops: Vec<Evidence>,
131 pub err_nil_checks: Vec<Evidence>,
133 pub try_catch_blocks: Vec<Evidence>,
135 pub error_enums: Vec<(String, Evidence)>,
137}
138
139impl ErrorHandlingSignals {
140 pub fn merge(&mut self, other: &ErrorHandlingSignals) {
142 self.try_except_blocks
143 .extend(other.try_except_blocks.clone());
144 self.custom_exceptions
145 .extend(other.custom_exceptions.clone());
146 self.result_types.extend(other.result_types.clone());
147 self.question_mark_ops
148 .extend(other.question_mark_ops.clone());
149 self.err_nil_checks.extend(other.err_nil_checks.clone());
150 self.try_catch_blocks.extend(other.try_catch_blocks.clone());
151 self.error_enums.extend(other.error_enums.clone());
152 }
153
154 pub fn has_signals(&self) -> bool {
156 !self.try_except_blocks.is_empty()
157 || !self.custom_exceptions.is_empty()
158 || !self.result_types.is_empty()
159 || !self.question_mark_ops.is_empty()
160 || !self.err_nil_checks.is_empty()
161 || !self.try_catch_blocks.is_empty()
162 || !self.error_enums.is_empty()
163 }
164
165 pub fn calculate_confidence(&self) -> f64 {
167 let mut confidence: f64 = 0.0;
168 if !self.try_except_blocks.is_empty() || !self.try_catch_blocks.is_empty() {
169 confidence += 0.3;
170 }
171 if !self.custom_exceptions.is_empty() || !self.error_enums.is_empty() {
172 confidence += 0.4;
173 }
174 if !self.result_types.is_empty() {
175 confidence += 0.4;
176 }
177 if !self.question_mark_ops.is_empty() {
178 confidence += 0.3;
179 }
180 if !self.err_nil_checks.is_empty() {
181 confidence += 0.4;
182 }
183 confidence.min(1.0)
184 }
185}
186
187#[derive(Debug, Clone, Default)]
193pub struct NamingSignals {
194 pub function_names: Vec<(String, NamingCase, String)>, pub class_names: Vec<(String, NamingCase, String)>,
198 pub constant_names: Vec<(String, NamingCase, String)>,
200 pub private_prefixes: HashMap<String, usize>, }
203
204#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
206pub enum NamingCase {
207 SnakeCase,
209 CamelCase,
211 PascalCase,
213 UpperSnakeCase,
215 Unknown,
217}
218
219impl NamingSignals {
220 pub fn merge(&mut self, other: &NamingSignals) {
222 self.function_names.extend(other.function_names.clone());
223 self.class_names.extend(other.class_names.clone());
224 self.constant_names.extend(other.constant_names.clone());
225 for (prefix, count) in &other.private_prefixes {
226 *self.private_prefixes.entry(prefix.clone()).or_insert(0) += count;
227 }
228 }
229
230 pub fn has_signals(&self) -> bool {
232 !self.function_names.is_empty()
233 || !self.class_names.is_empty()
234 || !self.constant_names.is_empty()
235 }
236}
237
238pub fn detect_naming_case(name: &str) -> NamingCase {
240 if name.is_empty() {
241 return NamingCase::Unknown;
242 }
243
244 if name.starts_with("__") && name.ends_with("__") {
246 return NamingCase::Unknown;
247 }
248 if name.len() == 1 {
249 return NamingCase::Unknown;
250 }
251
252 if name
254 .chars()
255 .all(|c| c.is_ascii_uppercase() || c == '_' || c.is_ascii_digit())
256 {
257 return NamingCase::UpperSnakeCase;
258 }
259
260 if name
262 .chars()
263 .all(|c| c.is_ascii_lowercase() || c == '_' || c.is_ascii_digit())
264 {
265 return NamingCase::SnakeCase;
266 }
267
268 let check_name = name.trim_start_matches('_');
270 if !check_name.is_empty() {
271 let first = check_name.chars().next().unwrap();
272 if first.is_ascii_uppercase() && !check_name.contains('_') {
273 return NamingCase::PascalCase;
274 }
275
276 if first.is_ascii_lowercase()
278 && !check_name.contains('_')
279 && check_name.chars().any(|c| c.is_ascii_uppercase())
280 {
281 return NamingCase::CamelCase;
282 }
283 }
284
285 NamingCase::Unknown
286}
287
288#[derive(Debug, Clone, Default)]
294pub struct ResourceManagementSignals {
295 pub context_managers: Vec<Evidence>,
297 pub enter_exit_methods: Vec<Evidence>,
299 pub defer_statements: Vec<Evidence>,
301 pub drop_impls: Vec<Evidence>,
303 pub try_finally_blocks: Vec<Evidence>,
305 pub close_calls: Vec<Evidence>,
307}
308
309impl ResourceManagementSignals {
310 pub fn merge(&mut self, other: &ResourceManagementSignals) {
312 self.context_managers.extend(other.context_managers.clone());
313 self.enter_exit_methods
314 .extend(other.enter_exit_methods.clone());
315 self.defer_statements.extend(other.defer_statements.clone());
316 self.drop_impls.extend(other.drop_impls.clone());
317 self.try_finally_blocks
318 .extend(other.try_finally_blocks.clone());
319 self.close_calls.extend(other.close_calls.clone());
320 }
321
322 pub fn has_signals(&self) -> bool {
324 !self.context_managers.is_empty()
325 || !self.enter_exit_methods.is_empty()
326 || !self.defer_statements.is_empty()
327 || !self.drop_impls.is_empty()
328 || !self.try_finally_blocks.is_empty()
329 || !self.close_calls.is_empty()
330 }
331
332 pub fn calculate_confidence(&self) -> f64 {
334 let mut confidence: f64 = 0.0;
335 if !self.context_managers.is_empty() {
336 confidence += 0.4;
337 }
338 if !self.enter_exit_methods.is_empty() {
339 confidence += 0.3;
340 }
341 if !self.defer_statements.is_empty() {
342 confidence += 0.4;
343 }
344 if !self.drop_impls.is_empty() {
345 confidence += 0.4;
346 }
347 if !self.try_finally_blocks.is_empty() {
348 confidence += 0.3;
349 }
350 if !self.close_calls.is_empty() {
351 confidence += 0.2;
352 }
353 confidence.min(1.0)
354 }
355}
356
357#[derive(Debug, Clone, Default)]
363pub struct ValidationSignals {
364 pub pydantic_models: Vec<Evidence>,
366 pub zod_schemas: Vec<Evidence>,
368 pub guard_clauses: Vec<Evidence>,
370 pub assert_statements: Vec<Evidence>,
372 pub type_checks: Vec<Evidence>,
374 pub other_validators: Vec<(String, Evidence)>,
376}
377
378impl ValidationSignals {
379 pub fn merge(&mut self, other: &ValidationSignals) {
381 self.pydantic_models.extend(other.pydantic_models.clone());
382 self.zod_schemas.extend(other.zod_schemas.clone());
383 self.guard_clauses.extend(other.guard_clauses.clone());
384 self.assert_statements
385 .extend(other.assert_statements.clone());
386 self.type_checks.extend(other.type_checks.clone());
387 self.other_validators.extend(other.other_validators.clone());
388 }
389
390 pub fn has_signals(&self) -> bool {
392 !self.pydantic_models.is_empty()
393 || !self.zod_schemas.is_empty()
394 || !self.guard_clauses.is_empty()
395 || !self.assert_statements.is_empty()
396 || !self.type_checks.is_empty()
397 || !self.other_validators.is_empty()
398 }
399
400 pub fn calculate_confidence(&self) -> f64 {
402 let mut confidence: f64 = 0.0;
403 if !self.pydantic_models.is_empty() {
404 confidence += 0.5;
405 }
406 if !self.zod_schemas.is_empty() {
407 confidence += 0.5;
408 }
409 if !self.guard_clauses.is_empty() {
410 confidence += 0.3;
411 }
412 if !self.assert_statements.is_empty() {
413 confidence += 0.2;
414 }
415 if !self.type_checks.is_empty() {
416 confidence += 0.2;
417 }
418 if !self.other_validators.is_empty() {
419 confidence += 0.3;
420 }
421 confidence.min(1.0)
422 }
423}
424
425#[derive(Debug, Clone, Default)]
431pub struct TestIdiomSignals {
432 pub pytest_fixtures: Vec<Evidence>,
434 pub mock_patches: Vec<Evidence>,
436 pub jest_blocks: Vec<Evidence>,
438 pub go_table_tests: Vec<Evidence>,
440 pub aaa_patterns: Vec<Evidence>,
442 pub test_function_count: usize,
444 pub detected_framework: Option<String>,
446}
447
448impl TestIdiomSignals {
449 pub fn merge(&mut self, other: &TestIdiomSignals) {
451 self.pytest_fixtures.extend(other.pytest_fixtures.clone());
452 self.mock_patches.extend(other.mock_patches.clone());
453 self.jest_blocks.extend(other.jest_blocks.clone());
454 self.go_table_tests.extend(other.go_table_tests.clone());
455 self.aaa_patterns.extend(other.aaa_patterns.clone());
456 self.test_function_count += other.test_function_count;
457 if self.detected_framework.is_none() {
458 self.detected_framework = other.detected_framework.clone();
459 }
460 }
461
462 pub fn has_signals(&self) -> bool {
464 !self.pytest_fixtures.is_empty()
465 || !self.mock_patches.is_empty()
466 || !self.jest_blocks.is_empty()
467 || !self.go_table_tests.is_empty()
468 || !self.aaa_patterns.is_empty()
469 || self.test_function_count > 0
470 }
471
472 pub fn calculate_confidence(&self) -> f64 {
474 let mut confidence: f64 = 0.0;
475 if !self.pytest_fixtures.is_empty() {
476 confidence += 0.4;
477 }
478 if !self.mock_patches.is_empty() {
479 confidence += 0.3;
480 }
481 if !self.jest_blocks.is_empty() {
482 confidence += 0.4;
483 }
484 if !self.go_table_tests.is_empty() {
485 confidence += 0.4;
486 }
487 if !self.aaa_patterns.is_empty() {
488 confidence += 0.3;
489 }
490 confidence.min(1.0)
491 }
492}
493
494#[derive(Debug, Clone, Default)]
500pub struct ImportPatternSignals {
501 pub absolute_imports: Vec<(String, String)>, pub relative_imports: Vec<(String, String)>,
505 pub star_imports: Vec<Evidence>,
507 pub aliases: HashMap<String, String>, pub groupings: Vec<ImportGrouping>,
511}
512
513#[derive(Debug, Clone)]
515pub struct ImportGrouping {
516 pub file: String,
518 pub stdlib_imports: Vec<String>,
520 pub third_party_imports: Vec<String>,
522 pub local_imports: Vec<String>,
524}
525
526impl ImportPatternSignals {
527 pub fn merge(&mut self, other: &ImportPatternSignals) {
529 self.absolute_imports.extend(other.absolute_imports.clone());
530 self.relative_imports.extend(other.relative_imports.clone());
531 self.star_imports.extend(other.star_imports.clone());
532 self.aliases.extend(other.aliases.clone());
533 self.groupings.extend(other.groupings.clone());
534 }
535
536 pub fn has_signals(&self) -> bool {
538 !self.absolute_imports.is_empty()
539 || !self.relative_imports.is_empty()
540 || !self.star_imports.is_empty()
541 }
542}
543
544#[derive(Debug, Clone, Default)]
550pub struct TypeCoverageSignals {
551 pub typed_params: usize,
553 pub untyped_params: usize,
555 pub typed_returns: usize,
557 pub untyped_returns: usize,
559 pub typed_variables: usize,
561 pub untyped_variables: usize,
563 pub generic_usage: Vec<Evidence>,
565 pub generic_patterns: HashSet<String>,
567}
568
569impl TypeCoverageSignals {
570 pub fn merge(&mut self, other: &TypeCoverageSignals) {
572 self.typed_params += other.typed_params;
573 self.untyped_params += other.untyped_params;
574 self.typed_returns += other.typed_returns;
575 self.untyped_returns += other.untyped_returns;
576 self.typed_variables += other.typed_variables;
577 self.untyped_variables += other.untyped_variables;
578 self.generic_usage.extend(other.generic_usage.clone());
579 self.generic_patterns.extend(other.generic_patterns.clone());
580 }
581
582 pub fn has_signals(&self) -> bool {
584 self.typed_params > 0
585 || self.typed_returns > 0
586 || self.typed_variables > 0
587 || self.untyped_params > 0
588 || self.untyped_returns > 0
589 }
590
591 pub fn calculate_function_coverage(&self) -> f64 {
593 let total =
594 self.typed_params + self.untyped_params + self.typed_returns + self.untyped_returns;
595 if total == 0 {
596 return 0.0;
597 }
598 (self.typed_params + self.typed_returns) as f64 / total as f64
599 }
600
601 pub fn calculate_variable_coverage(&self) -> f64 {
603 let total = self.typed_variables + self.untyped_variables;
604 if total == 0 {
605 return 0.0;
606 }
607 self.typed_variables as f64 / total as f64
608 }
609
610 pub fn calculate_overall_coverage(&self) -> f64 {
612 let total_typed = self.typed_params + self.typed_returns + self.typed_variables;
613 let total_untyped = self.untyped_params + self.untyped_returns + self.untyped_variables;
614 let total = total_typed + total_untyped;
615 if total == 0 {
616 return 0.0;
617 }
618 total_typed as f64 / total as f64
619 }
620}
621
622#[derive(Debug, Clone, Default)]
628pub struct ApiConventionSignals {
629 pub fastapi_decorators: Vec<Evidence>,
631 pub flask_decorators: Vec<Evidence>,
633 pub express_routes: Vec<Evidence>,
635 pub restful_patterns: Vec<Evidence>,
637 pub orm_models: Vec<(String, Evidence)>, pub graphql_defs: Vec<Evidence>,
641}
642
643impl ApiConventionSignals {
644 pub fn merge(&mut self, other: &ApiConventionSignals) {
646 self.fastapi_decorators
647 .extend(other.fastapi_decorators.clone());
648 self.flask_decorators.extend(other.flask_decorators.clone());
649 self.express_routes.extend(other.express_routes.clone());
650 self.restful_patterns.extend(other.restful_patterns.clone());
651 self.orm_models.extend(other.orm_models.clone());
652 self.graphql_defs.extend(other.graphql_defs.clone());
653 }
654
655 pub fn has_signals(&self) -> bool {
657 !self.fastapi_decorators.is_empty()
658 || !self.flask_decorators.is_empty()
659 || !self.express_routes.is_empty()
660 || !self.restful_patterns.is_empty()
661 || !self.orm_models.is_empty()
662 || !self.graphql_defs.is_empty()
663 }
664
665 pub fn calculate_confidence(&self) -> f64 {
667 let mut confidence: f64 = 0.0;
668 if !self.fastapi_decorators.is_empty() {
669 confidence += 0.5;
670 }
671 if !self.flask_decorators.is_empty() {
672 confidence += 0.5;
673 }
674 if !self.express_routes.is_empty() {
675 confidence += 0.5;
676 }
677 if !self.restful_patterns.is_empty() {
678 confidence += 0.3;
679 }
680 if !self.orm_models.is_empty() {
681 confidence += 0.4;
682 }
683 if !self.graphql_defs.is_empty() {
684 confidence += 0.4;
685 }
686 confidence.min(1.0)
687 }
688
689 pub fn detect_framework(&self) -> Option<String> {
691 if !self.fastapi_decorators.is_empty() {
692 return Some("fastapi".to_string());
693 }
694 if !self.flask_decorators.is_empty() {
695 return Some("flask".to_string());
696 }
697 if !self.express_routes.is_empty() {
698 return Some("express".to_string());
699 }
700 None
701 }
702
703 pub fn detect_orm(&self) -> Option<String> {
705 self.orm_models.first().map(|(orm, _)| orm.clone())
706 }
707}
708
709#[derive(Debug, Clone, Default)]
715pub struct AsyncPatternSignals {
716 pub async_await: Vec<Evidence>,
718 pub goroutines: Vec<Evidence>,
720 pub tokio_usage: Vec<Evidence>,
722 pub sync_primitives: Vec<(String, Evidence)>,
724 pub thread_spawns: Vec<Evidence>,
726}
727
728impl AsyncPatternSignals {
729 pub fn merge(&mut self, other: &AsyncPatternSignals) {
731 self.async_await.extend(other.async_await.clone());
732 self.goroutines.extend(other.goroutines.clone());
733 self.tokio_usage.extend(other.tokio_usage.clone());
734 self.sync_primitives.extend(other.sync_primitives.clone());
735 self.thread_spawns.extend(other.thread_spawns.clone());
736 }
737
738 pub fn has_signals(&self) -> bool {
740 !self.async_await.is_empty()
741 || !self.goroutines.is_empty()
742 || !self.tokio_usage.is_empty()
743 || !self.sync_primitives.is_empty()
744 || !self.thread_spawns.is_empty()
745 }
746
747 pub fn calculate_confidence(&self) -> f64 {
749 let mut confidence: f64 = 0.0;
750 if !self.async_await.is_empty() {
751 confidence += 0.4;
752 }
753 if !self.goroutines.is_empty() {
754 confidence += 0.5;
755 }
756 if !self.tokio_usage.is_empty() {
757 confidence += 0.5;
758 }
759 if !self.sync_primitives.is_empty() {
760 confidence += 0.3;
761 }
762 if !self.thread_spawns.is_empty() {
763 confidence += 0.3;
764 }
765 confidence.min(1.0)
766 }
767}