1use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard};
7use rayon::prelude::*;
8
9use ryo_analysis::context::AnalysisContext;
10use ryo_analysis::SymbolId;
11
12use crate::allow::AllowStore;
13use crate::id::SuggestId;
14use crate::store::{GcConfig, StoredSuggestion, SuggestIndex, SuggestStore};
15use crate::suggest::{
16 LintSeverity, MutationSpec, ParamDef, SafetyLevel, SuggestCategory, SuggestOpportunity,
17 SuggestParams, SymbolScope,
18};
19use crate::suggest_registry::SuggestRegistry;
20use crate::trigger::{AcChanges, PendingChanges, SuggestStrategy, SuggestTrigger};
21
22#[derive(Debug, Clone)]
27pub struct ParameterizedSuggestInfo {
28 pub name: &'static str,
30 pub description: String,
32 pub category: SuggestCategory,
34 pub param_schema: Vec<ParamDef>,
36}
37
38#[derive(Debug, Clone, Default)]
40pub struct SuggestQuery {
41 pub category: Option<SuggestCategory>,
43
44 pub max_safety: Option<SafetyLevel>,
46
47 pub min_confidence: Option<f32>,
49
50 pub target_symbols: Option<Vec<SymbolId>>,
52
53 pub limit: Option<usize>,
55}
56
57impl SuggestQuery {
58 pub fn all() -> Self {
60 Self::default()
61 }
62
63 pub fn with_category(mut self, category: SuggestCategory) -> Self {
65 self.category = Some(category);
66 self
67 }
68
69 pub fn with_max_safety(mut self, safety: SafetyLevel) -> Self {
71 self.max_safety = Some(safety);
72 self
73 }
74
75 pub fn with_min_confidence(mut self, confidence: f32) -> Self {
77 self.min_confidence = Some(confidence);
78 self
79 }
80
81 pub fn with_targets(mut self, symbols: Vec<SymbolId>) -> Self {
83 self.target_symbols = Some(symbols);
84 self
85 }
86
87 pub fn with_limit(mut self, limit: usize) -> Self {
89 self.limit = Some(limit);
90 self
91 }
92
93 fn matches(&self, stored: &StoredSuggestion, registry: &SuggestRegistry) -> bool {
95 if let Some(cat) = self.category {
97 if let Some(suggest) = registry.get(stored.suggest_idx) {
98 if suggest.category() != cat {
99 return false;
100 }
101 }
102 }
103
104 if let Some(max_safety) = self.max_safety {
106 if stored.safety > max_safety {
107 return false;
108 }
109 }
110
111 if let Some(min_conf) = self.min_confidence {
113 if stored.opportunity.confidence < min_conf {
114 return false;
115 }
116 }
117
118 if let Some(ref targets) = self.target_symbols {
120 let has_match = stored
121 .opportunity
122 .targets
123 .iter()
124 .any(|t| targets.contains(t));
125 if !has_match {
126 return false;
127 }
128 }
129
130 true
131 }
132}
133
134#[derive(Debug)]
136pub struct SuggestView<'a> {
137 pub id: SuggestId,
139
140 pub stored: &'a StoredSuggestion,
142
143 pub pattern_name: &'static str,
145
146 pub category: SuggestCategory,
148}
149
150pub struct SuggestService {
155 store: RwLock<SuggestStore>,
157
158 registry: SuggestRegistry,
160
161 strategy: SuggestStrategy,
163
164 pending: RwLock<PendingChanges>,
166
167 gc_config: GcConfig,
169}
170
171impl SuggestService {
172 pub fn new(registry: SuggestRegistry) -> Self {
174 Self {
175 store: RwLock::new(SuggestStore::new()),
176 registry,
177 strategy: SuggestStrategy::default(),
178 pending: RwLock::new(PendingChanges::new()),
179 gc_config: GcConfig::default(),
180 }
181 }
182
183 pub fn with_strategy(registry: SuggestRegistry, strategy: SuggestStrategy) -> Self {
185 Self {
186 store: RwLock::new(SuggestStore::new()),
187 registry,
188 strategy,
189 pending: RwLock::new(PendingChanges::new()),
190 gc_config: GcConfig::default(),
191 }
192 }
193
194 pub fn with_gc_config(registry: SuggestRegistry, gc_config: GcConfig) -> Self {
196 Self {
197 store: RwLock::new(SuggestStore::new()),
198 registry,
199 strategy: SuggestStrategy::default(),
200 pending: RwLock::new(PendingChanges::new()),
201 gc_config,
202 }
203 }
204
205 pub fn get(&self, id: SuggestId) -> Option<MappedRwLockReadGuard<'_, StoredSuggestion>> {
212 let guard = self.store.read();
213 RwLockReadGuard::try_map(guard, |store| store.get(id)).ok()
214 }
215
216 pub fn is_valid(&self, id: SuggestId) -> bool {
218 self.store.read().is_valid(id)
219 }
220
221 pub fn query(&self, query: &SuggestQuery) -> Vec<(SuggestId, SuggestCategory, SafetyLevel)> {
223 let store = self.store.read();
224
225 let mut results: Vec<_> = store
226 .iter()
227 .filter(|(_, stored)| query.matches(stored, &self.registry))
228 .map(|(id, stored)| {
229 let category = self
230 .registry
231 .get(stored.suggest_idx)
232 .map(|s| s.category())
233 .unwrap_or(SuggestCategory::Refactor);
234 (id, category, stored.safety)
235 })
236 .collect();
237
238 if let Some(limit) = query.limit {
240 results.truncate(limit);
241 }
242
243 results
244 }
245
246 pub fn auto_applicable(&self) -> Vec<SuggestId> {
248 self.query(&SuggestQuery::all().with_max_safety(SafetyLevel::Auto))
249 .into_iter()
250 .map(|(id, _, _)| id)
251 .collect()
252 }
253
254 pub fn by_category(&self, category: SuggestCategory) -> Vec<SuggestId> {
256 self.query(&SuggestQuery::all().with_category(category))
257 .into_iter()
258 .map(|(id, _, _)| id)
259 .collect()
260 }
261
262 pub fn count(&self) -> usize {
264 self.store.read().len()
265 }
266
267 pub fn is_empty(&self) -> bool {
269 self.count() == 0
270 }
271
272 pub fn pattern_name(&self, id: SuggestId) -> Option<&'static str> {
274 let store = self.store.read();
275 let stored = store.get(id)?;
276 self.registry.get(stored.suggest_idx).map(|s| s.name())
277 }
278
279 pub fn rule_id(&self, id: SuggestId) -> Option<&str> {
282 let store = self.store.read();
283 let stored = store.get(id)?;
284 self.registry
285 .get(stored.suggest_idx)
286 .and_then(|s| s.rule_id())
287 }
288
289 pub fn registry(&self) -> &SuggestRegistry {
291 &self.registry
292 }
293
294 pub fn to_mutation_specs(
300 &self,
301 id: SuggestId,
302 ctx: &AnalysisContext,
303 ) -> Option<Vec<MutationSpec>> {
304 let store = self.store.read();
305 let stored = store.get(id)?;
306 let suggest = self.registry.get(stored.suggest_idx)?;
307
308 suggest.to_mutation_specs(ctx, &stored.opportunity).ok()
309 }
310
311 pub fn generate_with_params(
334 &self,
335 ctx: &AnalysisContext,
336 rule_id: &str,
337 params: &SuggestParams,
338 ) -> Option<Vec<SuggestOpportunity>> {
339 let (_, suggest) = self.registry.get_by_name(rule_id)?;
340
341 if !suggest.accepts_params() {
342 return None;
343 }
344
345 Some(suggest.detect_with_params(ctx, &[], params))
346 }
347
348 pub fn generate_and_store(
355 &self,
356 ctx: &AnalysisContext,
357 rule_id: &str,
358 params: &SuggestParams,
359 ) -> usize {
360 let Some((idx, suggest)) = self.registry.get_by_name(rule_id) else {
361 return 0;
362 };
363
364 if !suggest.accepts_params() {
365 return 0;
366 }
367
368 let opportunities = suggest.detect_with_params(ctx, &[], params);
369 let mut count = 0;
370
371 for opportunity in opportunities {
372 let stored = StoredSuggestion::new(
373 opportunity,
374 idx,
375 suggest.safety_level(),
376 suggest.priority_weight(),
377 );
378 if self.insert(stored).is_some() {
379 count += 1;
380 }
381 }
382
383 count
384 }
385
386 pub fn list_parameterized(&self) -> Vec<ParameterizedSuggestInfo> {
392 self.registry
393 .iter()
394 .filter_map(|(_, suggest)| {
395 if suggest.accepts_params() {
396 Some(ParameterizedSuggestInfo {
397 name: suggest.name(),
398 description: suggest.description().to_string(),
399 category: suggest.category(),
400 param_schema: suggest.param_schema(),
401 })
402 } else {
403 None
404 }
405 })
406 .collect()
407 }
408
409 pub fn record_changes(&self, trigger: SuggestTrigger) -> bool {
413 let mut pending = self.pending.write();
414
415 if let Some(changes) = trigger.changes() {
417 pending.record_goal(changes.clone());
418 }
419
420 self.strategy.should_evaluate(&trigger, &pending)
422 }
423
424 pub fn take_pending(&self) -> (usize, AcChanges) {
426 self.pending.write().take()
427 }
428
429 pub fn insert(&self, suggestion: StoredSuggestion) -> Option<SuggestId> {
434 self.store.write().insert(suggestion)
435 }
436
437 pub fn close(&self, id: SuggestId, reason: impl Into<String>) -> bool {
439 self.store.write().close(id, reason)
440 }
441
442 pub fn invalidate_for_symbol(&self, symbol: &SymbolId) {
444 self.store.write().invalidate_for_symbol(symbol);
445 }
446
447 pub fn remove_for_symbol(&self, symbol: &SymbolId) {
449 self.store.write().remove_for_symbol(symbol);
450 }
451
452 pub fn gc(&self, valid_symbols: impl Fn(&SymbolId) -> bool) {
454 self.store.write().gc(&self.gc_config, &valid_symbols);
455 }
456
457 pub fn clear(&self) {
459 self.store.write().clear();
460 }
461
462 pub fn detect(&self, ctx: &AnalysisContext, symbols: &[SymbolId]) -> usize {
471 let allow_store = AllowStore::from_context(ctx);
473 self.detect_with_allow(ctx, symbols, &allow_store)
474 }
475
476 pub fn detect_with_allow(
481 &self,
482 ctx: &AnalysisContext,
483 symbols: &[SymbolId],
484 allow_store: &AllowStore,
485 ) -> usize {
486 let mut count = 0;
487
488 for (idx, suggest) in self.registry.iter() {
489 let rule_id = suggest.rule_id();
490 let opportunities = suggest.detect(ctx, symbols);
491
492 for opportunity in opportunities {
493 if let Some(rule_id) = rule_id {
495 if allow_store.is_allowed_for_symbols(ctx, &opportunity.targets, rule_id) {
496 continue; }
498 }
499
500 let stored = StoredSuggestion::new(
501 opportunity,
502 idx,
503 suggest.safety_level(),
504 suggest.priority_weight(),
505 );
506 if self.insert(stored).is_some() {
507 count += 1;
508 }
509 }
510 }
511
512 count
513 }
514
515 pub fn detect_with_rule_filter<F>(
520 &self,
521 ctx: &AnalysisContext,
522 symbols: &[SymbolId],
523 is_rule_enabled: F,
524 ) -> usize
525 where
526 F: Fn(&str, &str) -> bool,
527 {
528 let allow_store = AllowStore::from_context(ctx);
529 self.detect_with_allow_and_rule_filter(ctx, symbols, &allow_store, is_rule_enabled)
530 }
531
532 pub fn detect_with_allow_and_rule_filter<F>(
537 &self,
538 ctx: &AnalysisContext,
539 symbols: &[SymbolId],
540 allow_store: &AllowStore,
541 is_rule_enabled: F,
542 ) -> usize
543 where
544 F: Fn(&str, &str) -> bool,
545 {
546 self.detect_with_config(ctx, symbols, allow_store, is_rule_enabled, |_| None)
547 }
548
549 pub fn detect_with_config<F, S>(
562 &self,
563 ctx: &AnalysisContext,
564 symbols: &[SymbolId],
565 allow_store: &AllowStore,
566 is_rule_enabled: F,
567 severity_override: S,
568 ) -> usize
569 where
570 F: Fn(&str, &str) -> bool,
571 S: Fn(&str) -> Option<LintSeverity>,
572 {
573 self.detect_with_config_and_scope(
574 ctx,
575 symbols,
576 allow_store,
577 is_rule_enabled,
578 severity_override,
579 &[],
580 )
581 }
582
583 pub fn detect_with_config_and_scope<F, S>(
590 &self,
591 ctx: &AnalysisContext,
592 symbols: &[SymbolId],
593 allow_store: &AllowStore,
594 is_rule_enabled: F,
595 severity_override: S,
596 scope_filter: &[SymbolScope],
597 ) -> usize
598 where
599 F: Fn(&str, &str) -> bool,
600 S: Fn(&str) -> Option<LintSeverity>,
601 {
602 let mut count = 0;
603
604 let binary_crates = SymbolScope::binary_crate_names(ctx);
606
607 for (idx, suggest) in self.registry.iter() {
608 let rule_id = suggest.rule_id();
609
610 let opportunities = suggest.detect(ctx, symbols);
611
612 for opportunity in opportunities {
613 if let Some(rule_id) = rule_id {
615 let file_path = &opportunity.location.file;
616 if !is_rule_enabled(rule_id, file_path) {
617 continue;
618 }
619 }
620
621 if let Some(rule_id) = rule_id {
623 if allow_store.is_allowed_for_symbols(ctx, &opportunity.targets, rule_id) {
624 continue;
625 }
626 }
627
628 let scope = opportunity
630 .primary_target()
631 .map(|sid| SymbolScope::resolve(ctx, sid, &binary_crates))
632 .unwrap_or_default();
633
634 if !scope_filter.is_empty() && !scope_filter.contains(&scope) {
636 continue;
637 }
638
639 let target_scopes = suggest.target_scopes();
641 if !target_scopes.is_empty() && !target_scopes.contains(&scope) {
642 continue;
643 }
644
645 let opportunity = opportunity.with_scope(scope);
647
648 let opportunity = if let Some(rule_id) = rule_id {
650 if let Some(new_severity) = severity_override(rule_id) {
651 opportunity.with_severity_override(new_severity)
652 } else {
653 opportunity
654 }
655 } else {
656 opportunity
657 };
658
659 let safety = opportunity
663 .lint_severity()
664 .map(SafetyLevel::from)
665 .unwrap_or_else(|| suggest.safety_level());
666
667 let stored =
668 StoredSuggestion::new(opportunity, idx, safety, suggest.priority_weight());
669 if self.insert(stored).is_some() {
670 count += 1;
671 }
672 }
673 }
674
675 count
676 }
677
678 pub fn detect_patterns(
682 &self,
683 ctx: &AnalysisContext,
684 symbols: &[SymbolId],
685 pattern_names: &[&str],
686 ) -> usize {
687 let allow_store = AllowStore::from_context(ctx);
688 self.detect_patterns_with_allow(ctx, symbols, pattern_names, &allow_store)
689 }
690
691 pub fn detect_patterns_with_allow(
693 &self,
694 ctx: &AnalysisContext,
695 symbols: &[SymbolId],
696 pattern_names: &[&str],
697 allow_store: &AllowStore,
698 ) -> usize {
699 let mut count = 0;
700
701 for name in pattern_names {
702 if let Some((idx, suggest)) = self.registry.get_by_name(name) {
703 let rule_id = suggest.rule_id();
704 let opportunities = suggest.detect(ctx, symbols);
705
706 for opportunity in opportunities {
707 if let Some(rule_id) = rule_id {
709 if allow_store.is_allowed_for_symbols(ctx, &opportunity.targets, rule_id) {
710 continue;
711 }
712 }
713
714 let stored = StoredSuggestion::new(
715 opportunity,
716 idx,
717 suggest.safety_level(),
718 suggest.priority_weight(),
719 );
720 if self.insert(stored).is_some() {
721 count += 1;
722 }
723 }
724 }
725 }
726
727 count
728 }
729
730 pub fn detect_with_precheck<F>(
748 &self,
749 ctx: &AnalysisContext,
750 symbols: &[SymbolId],
751 precheck: F,
752 ) -> DetectWithPrecheckResult
753 where
754 F: Fn(&SuggestOpportunity, &[MutationSpec], &AnalysisContext) -> bool,
755 {
756 let allow_store = AllowStore::from_context(ctx);
757 self.detect_with_precheck_and_allow(ctx, symbols, &allow_store, precheck)
758 }
759
760 pub fn detect_with_precheck_and_allow<F>(
762 &self,
763 ctx: &AnalysisContext,
764 symbols: &[SymbolId],
765 allow_store: &AllowStore,
766 precheck: F,
767 ) -> DetectWithPrecheckResult
768 where
769 F: Fn(&SuggestOpportunity, &[MutationSpec], &AnalysisContext) -> bool,
770 {
771 let mut result = DetectWithPrecheckResult::default();
772
773 for (idx, suggest) in self.registry.iter() {
774 let rule_id = suggest.rule_id();
775 let opportunities = suggest.detect(ctx, symbols);
776
777 for opportunity in opportunities {
778 if let Some(rule_id) = rule_id {
780 if allow_store.is_allowed_for_symbols(ctx, &opportunity.targets, rule_id) {
781 continue; }
783 }
784
785 let specs = match suggest.to_mutation_specs(ctx, &opportunity) {
787 Ok(s) => s,
788 Err(_) => {
789 result.skipped_no_specs += 1;
790 continue;
791 }
792 };
793
794 if specs.is_empty() {
795 result.skipped_no_specs += 1;
796 continue;
797 }
798
799 if precheck(&opportunity, &specs, ctx) {
801 let stored = StoredSuggestion::new(
802 opportunity,
803 idx,
804 suggest.safety_level(),
805 suggest.priority_weight(),
806 );
807 if self.insert(stored).is_some() {
808 result.passed += 1;
809 }
810 } else {
811 result.failed_precheck += 1;
812 }
813 }
814 }
815
816 result
817 }
818
819 pub fn detect_with_parallel_precheck<F>(
839 &self,
840 ctx: &AnalysisContext,
841 symbols: &[SymbolId],
842 precheck: F,
843 ) -> DetectWithPrecheckResult
844 where
845 F: Fn(&SuggestOpportunity, &[MutationSpec], &AnalysisContext) -> bool + Sync,
846 {
847 self.detect_with_parallel_precheck_limited(ctx, symbols, None, precheck)
848 }
849
850 pub fn detect_with_parallel_precheck_limited<F>(
855 &self,
856 ctx: &AnalysisContext,
857 symbols: &[SymbolId],
858 limit: Option<usize>,
859 precheck: F,
860 ) -> DetectWithPrecheckResult
861 where
862 F: Fn(&SuggestOpportunity, &[MutationSpec], &AnalysisContext) -> bool + Sync,
863 {
864 let allow_store = AllowStore::from_context(ctx);
865 self.detect_with_parallel_precheck_and_allow_limited(
866 ctx,
867 symbols,
868 &allow_store,
869 limit,
870 precheck,
871 )
872 }
873
874 pub fn detect_with_parallel_precheck_and_allow<F>(
876 &self,
877 ctx: &AnalysisContext,
878 symbols: &[SymbolId],
879 allow_store: &AllowStore,
880 precheck: F,
881 ) -> DetectWithPrecheckResult
882 where
883 F: Fn(&SuggestOpportunity, &[MutationSpec], &AnalysisContext) -> bool + Sync,
884 {
885 self.detect_with_parallel_precheck_and_allow_limited(
886 ctx,
887 symbols,
888 allow_store,
889 None,
890 precheck,
891 )
892 }
893
894 pub fn detect_with_parallel_precheck_and_allow_limited<F>(
896 &self,
897 ctx: &AnalysisContext,
898 symbols: &[SymbolId],
899 allow_store: &AllowStore,
900 limit: Option<usize>,
901 precheck: F,
902 ) -> DetectWithPrecheckResult
903 where
904 F: Fn(&SuggestOpportunity, &[MutationSpec], &AnalysisContext) -> bool + Sync,
905 {
906 self.detect_with_parallel_precheck_full(
907 ctx,
908 symbols,
909 allow_store,
910 limit,
911 |_| true,
912 precheck,
913 )
914 }
915
916 pub fn detect_with_parallel_precheck_and_rule_filter<F, R>(
924 &self,
925 ctx: &AnalysisContext,
926 symbols: &[SymbolId],
927 limit: Option<usize>,
928 is_rule_enabled: R,
929 precheck: F,
930 ) -> DetectWithPrecheckResult
931 where
932 F: Fn(&SuggestOpportunity, &[MutationSpec], &AnalysisContext) -> bool + Sync,
933 R: Fn(&str) -> bool,
934 {
935 let allow_store = AllowStore::from_context(ctx);
936 self.detect_with_parallel_precheck_full(
937 ctx,
938 symbols,
939 &allow_store,
940 limit,
941 is_rule_enabled,
942 precheck,
943 )
944 }
945
946 pub fn detect_with_parallel_precheck_full<F, R>(
950 &self,
951 ctx: &AnalysisContext,
952 symbols: &[SymbolId],
953 allow_store: &AllowStore,
954 limit: Option<usize>,
955 is_rule_enabled: R,
956 precheck: F,
957 ) -> DetectWithPrecheckResult
958 where
959 F: Fn(&SuggestOpportunity, &[MutationSpec], &AnalysisContext) -> bool + Sync,
960 R: Fn(&str) -> bool,
961 {
962 let mut candidates: Vec<(
965 SuggestOpportunity,
966 Vec<MutationSpec>,
967 SuggestIndex,
968 SafetyLevel,
969 u8,
970 )> = Vec::new();
971 let mut skipped_no_specs = 0;
972
973 for (idx, suggest) in self.registry.iter() {
974 let rule_id = suggest.rule_id();
975
976 if let Some(rule_id) = rule_id {
978 if !is_rule_enabled(rule_id) {
979 continue;
980 }
981 }
982
983 let opportunities = suggest.detect(ctx, symbols);
984
985 for opportunity in opportunities {
986 if let Some(rule_id) = rule_id {
988 if allow_store.is_allowed_for_symbols(ctx, &opportunity.targets, rule_id) {
989 continue;
990 }
991 }
992
993 let specs = match suggest.to_mutation_specs(ctx, &opportunity) {
995 Ok(s) => s,
996 Err(_) => {
997 skipped_no_specs += 1;
998 continue;
999 }
1000 };
1001
1002 if specs.is_empty() {
1003 skipped_no_specs += 1;
1004 continue;
1005 }
1006
1007 let safety = suggest.safety_level();
1008 let priority = crate::suggest::compute_priority(
1009 opportunity.confidence,
1010 safety,
1011 suggest.priority_weight(),
1012 );
1013 candidates.push((opportunity, specs, idx, safety, priority));
1014 }
1015 }
1016
1017 candidates.sort_by_key(|b| std::cmp::Reverse(b.4)); let total_candidates = candidates.len();
1020 let skipped_by_limit = if let Some(limit) = limit {
1021 if candidates.len() > limit {
1022 let skipped = candidates.len() - limit;
1023 candidates.truncate(limit);
1024 skipped
1025 } else {
1026 0
1027 }
1028 } else {
1029 0
1030 };
1031
1032 let precheck_results: Vec<Option<StoredSuggestion>> = candidates
1035 .into_par_iter()
1036 .map(|(opportunity, specs, suggest_idx, safety, priority)| {
1037 if precheck(&opportunity, &specs, ctx) {
1038 let mut stored = StoredSuggestion::new_with_priority(
1039 opportunity,
1040 suggest_idx,
1041 safety,
1042 priority,
1043 );
1044 stored.precheck_status = crate::store::PrecheckStatus::Passed;
1045 Some(stored)
1046 } else {
1047 None
1048 }
1049 })
1050 .collect();
1051
1052 let mut passed = 0;
1054 let mut failed_precheck = 0;
1055
1056 for result in precheck_results {
1057 match result {
1058 Some(stored) => {
1059 if self.insert(stored).is_some() {
1060 passed += 1;
1061 }
1062 }
1063 None => {
1064 failed_precheck += 1;
1065 }
1066 }
1067 }
1068
1069 DetectWithPrecheckResult {
1070 passed,
1071 failed_precheck,
1072 skipped_no_specs,
1073 skipped_by_limit,
1074 total_candidates,
1075 }
1076 }
1077}
1078
1079#[derive(Debug, Clone, Default)]
1081pub struct DetectWithPrecheckResult {
1082 pub passed: usize,
1084 pub failed_precheck: usize,
1086 pub skipped_no_specs: usize,
1088 pub skipped_by_limit: usize,
1090 pub total_candidates: usize,
1092}
1093
1094impl DetectWithPrecheckResult {
1095 pub fn total_detected(&self) -> usize {
1097 self.passed + self.failed_precheck + self.skipped_no_specs
1098 }
1099
1100 pub fn prechecked(&self) -> usize {
1102 self.passed + self.failed_precheck
1103 }
1104}
1105
1106#[derive(Debug, Clone, Default)]
1108pub struct SuggestStats {
1109 pub active_count: usize,
1111
1112 pub by_category: std::collections::HashMap<SuggestCategory, usize>,
1114
1115 pub by_safety: std::collections::HashMap<SafetyLevel, usize>,
1117
1118 pub pattern_count: usize,
1120}
1121
1122impl SuggestService {
1123 pub fn stats(&self) -> SuggestStats {
1125 let mut stats = SuggestStats {
1126 pattern_count: self.registry.len(),
1127 ..SuggestStats::default()
1128 };
1129
1130 let store = self.store.read();
1131
1132 for (_, stored) in store.iter() {
1133 stats.active_count += 1;
1134
1135 if let Some(suggest) = self.registry.get(stored.suggest_idx) {
1137 *stats.by_category.entry(suggest.category()).or_default() += 1;
1138 }
1139
1140 *stats.by_safety.entry(stored.safety).or_default() += 1;
1142 }
1143
1144 stats
1145 }
1146
1147 pub fn take_store(&self) -> SuggestStore {
1151 std::mem::take(&mut *self.store.write())
1152 }
1153
1154 pub fn restore_store(&self, store: SuggestStore) {
1158 *self.store.write() = store;
1159 }
1160}
1161
1162#[cfg(test)]
1163mod tests {
1164 use super::*;
1165
1166 #[test]
1167 fn test_suggest_query_builder() {
1168 let query = SuggestQuery::all()
1169 .with_category(SuggestCategory::Derive)
1170 .with_max_safety(SafetyLevel::Confirm)
1171 .with_min_confidence(0.8)
1172 .with_limit(10);
1173
1174 assert_eq!(query.category, Some(SuggestCategory::Derive));
1175 assert_eq!(query.max_safety, Some(SafetyLevel::Confirm));
1176 assert_eq!(query.min_confidence, Some(0.8));
1177 assert_eq!(query.limit, Some(10));
1178 }
1179
1180 #[test]
1181 fn test_service_new() {
1182 let registry = SuggestRegistry::new();
1183 let service = SuggestService::new(registry);
1184
1185 assert!(service.is_empty());
1186 assert_eq!(service.count(), 0);
1187 }
1188
1189 #[test]
1190 fn test_service_with_strategy() {
1191 let registry = SuggestRegistry::new();
1192 let strategy = SuggestStrategy::high_perf();
1193 let service = SuggestService::with_strategy(registry, strategy);
1194
1195 assert!(service.is_empty());
1196 }
1197}