1use ryo_analysis::context::AnalysisContext;
6use ryo_analysis::{SymbolId, SymbolKind, SymbolPath};
7use ryo_pattern::{BodyScanner, MatchResult, Rule, Severity as PatternSeverity};
8use ryo_source::pure::{
9 PureBlock, PureExpr, PureItem, PureMatchArm, PurePattern, PureStmt, PureVis,
10};
11
12use crate::lint::{LintDetails, LintSuggest};
13use crate::suggest::SymbolScope;
14use crate::{
15 EnumToTraitStrategy, LintSeverity, MatchHandling, MutationSpec, OpportunityId, SafetyLevel,
16 Suggest, SuggestCategory, SuggestLocation, SuggestOpportunity, SuggestResult,
17};
18
19pub struct PatternBasedSuggest {
36 rule: Rule,
37 name: &'static str,
39 description: String,
41}
42
43impl PatternBasedSuggest {
44 pub fn new(rule: Rule) -> Self {
46 let name = Box::leak(rule.id.clone().into_boxed_str());
49 let description = rule.name.clone();
50 Self {
51 rule,
52 name,
53 description,
54 }
55 }
56
57 pub fn rule(&self) -> &Rule {
59 &self.rule
60 }
61
62 fn to_lint_severity(severity: PatternSeverity) -> LintSeverity {
64 match severity {
65 PatternSeverity::Error => LintSeverity::Error,
66 PatternSeverity::Warning => LintSeverity::Warning,
67 PatternSeverity::Info | PatternSeverity::Hint => LintSeverity::Info,
68 }
69 }
70
71 fn to_safety_level(severity: PatternSeverity) -> SafetyLevel {
73 match severity {
74 PatternSeverity::Error => SafetyLevel::Manual,
75 PatternSeverity::Warning => SafetyLevel::Confirm,
76 PatternSeverity::Info | PatternSeverity::Hint => SafetyLevel::Auto,
77 }
78 }
79
80 fn matches_kind(&self, ctx: &AnalysisContext, symbol_id: SymbolId) -> bool {
82 let query_kind = match &self.rule.query.kind {
83 Some(k) => k,
84 None => return true, };
86
87 let symbol_kind = match ctx.registry.kind(symbol_id) {
88 Some(k) => k,
89 None => return false,
90 };
91
92 use ryo_pattern::SymbolKind as PatternKind;
94 matches!(
95 (query_kind, symbol_kind),
96 (PatternKind::Function, SymbolKind::Function)
97 | (PatternKind::Struct, SymbolKind::Struct)
98 | (PatternKind::Enum, SymbolKind::Enum)
99 | (PatternKind::Trait, SymbolKind::Trait)
100 | (PatternKind::Impl, SymbolKind::Impl)
101 | (PatternKind::Mod, SymbolKind::Mod)
102 | (PatternKind::Const, SymbolKind::Const)
103 | (PatternKind::Static, SymbolKind::Static)
104 | (PatternKind::TypeAlias, SymbolKind::TypeAlias)
105 | (PatternKind::Field, SymbolKind::Field)
106 | (PatternKind::Variant, SymbolKind::Variant)
107 )
108 }
109
110 fn matches_attrs(&self, ctx: &AnalysisContext, symbol_id: SymbolId) -> bool {
112 let attrs = match &self.rule.query.r#match {
113 Some(a) => a,
114 None => return true, };
116
117 if let Some(ref name_pattern) = attrs.name {
119 if let Some(path) = ctx.registry.path(symbol_id) {
120 let name = path.name();
121 if name != name_pattern {
122 return false;
123 }
124 }
125 }
126
127 if let Some(ref pattern) = attrs.pattern {
129 if let Some(path) = ctx.registry.path(symbol_id) {
130 let name = path.name();
131 if !self.matches_glob(pattern, name) {
132 return false;
133 }
134 }
135 }
136
137 if let Some(ref vis) = attrs.vis {
139 let ast_vis = Self::get_ast_visibility(ctx, symbol_id);
140 use ryo_pattern::Visibility;
141 match vis {
142 Visibility::Public => {
143 if ast_vis != PureVis::Public {
144 return false;
145 }
146 }
147 Visibility::Crate => {
148 if ast_vis != PureVis::Crate {
149 return false;
150 }
151 }
152 Visibility::Super => {
153 if ast_vis != PureVis::Super {
154 return false;
155 }
156 }
157 Visibility::Private => {
158 if ast_vis != PureVis::Private {
159 return false;
160 }
161 }
162 }
163 }
164
165 true
166 }
167
168 fn get_ast_visibility(ctx: &AnalysisContext, symbol_id: SymbolId) -> PureVis {
171 if let Some(item) = ctx.ast_registry.get(symbol_id) {
172 match item {
173 PureItem::Fn(f) => return f.vis.clone(),
174 PureItem::Struct(s) => return s.vis.clone(),
175 PureItem::Enum(e) => return e.vis.clone(),
176 PureItem::Const(c) => return c.vis.clone(),
177 PureItem::Static(s) => return s.vis.clone(),
178 PureItem::Trait(t) => return t.vis.clone(),
179 PureItem::Type(t) => return t.vis.clone(),
180 PureItem::Mod(m) => return m.vis.clone(),
181 _ => {}
182 }
183 }
184 PureVis::Private
185 }
186
187 fn matches_glob(&self, pattern: &str, name: &str) -> bool {
189 if pattern == "*" {
190 return true;
191 }
192 if let Some(prefix) = pattern.strip_suffix('*') {
193 return name.starts_with(prefix);
194 }
195 if let Some(suffix) = pattern.strip_prefix('*') {
196 return name.ends_with(suffix);
197 }
198 pattern == name
199 }
200
201 fn matches_dynamic_checks(
206 &self,
207 ctx: &AnalysisContext,
208 symbol_id: SymbolId,
209 ) -> (bool, Option<String>) {
210 match self.rule.id.as_str() {
211 "RL060" => {
213 let impl_count = ctx.code_graph().impl_count(symbol_id);
214 if impl_count == 1 {
215 let impl_id = match ctx.code_graph().implementors_of(symbol_id).next() {
216 Some(id) => id,
217 None => return (false, None),
218 };
219
220 if let Some(detail) = ctx.detail_store().impl_(impl_id) {
223 if !detail.generics.type_params.is_empty()
224 && detail
225 .generics
226 .type_params
227 .iter()
228 .any(|tp| tp == &detail.self_ty)
229 {
230 return (false, None);
231 }
232 }
233
234 let implementor_name = ctx
235 .registry
236 .path(impl_id)
237 .map(|p| p.name().to_string())
238 .unwrap_or_else(|| "unknown".to_string());
239
240 let trait_name = ctx
241 .registry
242 .path(symbol_id)
243 .map(|p| p.name().to_string())
244 .unwrap_or_else(|| "unknown".to_string());
245
246 let message = format!(
247 "Trait `{}` has exactly 1 implementor (`{}`). Consider InlineTrait to simplify.",
248 trait_name, implementor_name
249 );
250 (true, Some(message))
251 } else {
252 (false, None)
254 }
255 }
256
257 "RL061" => {
260 const MIN_METHODS: usize = 5;
261
262 let detail = ctx.detail_store().impl_(symbol_id);
263
264 let is_inherent = detail.map(|d| d.trait_.is_none()).unwrap_or(false);
266 if !is_inherent {
267 return (false, None);
268 }
269
270 let self_ty = detail.map(|d| d.self_ty.as_str()).unwrap_or("unknown");
271 let method_count = detail.map(|d| d.methods.len()).unwrap_or(0);
272
273 if method_count < MIN_METHODS {
274 return (false, None);
275 }
276
277 let has_trait_impl = ctx
279 .registry
280 .iter_by_kind(SymbolKind::Impl)
281 .filter(|&id| id != symbol_id)
282 .any(|id| {
283 ctx.detail_store()
284 .impl_(id)
285 .map(|d| d.trait_.is_some() && d.self_ty == self_ty)
286 .unwrap_or(false)
287 });
288
289 if has_trait_impl {
290 return (false, None);
291 }
292
293 let message = format!(
294 "Inherent impl for `{}` with {} methods, no trait impls. Consider ExtractTrait.",
295 self_ty, method_count
296 );
297 (true, Some(message))
298 }
299
300 "RL043" => {
302 let has_trailing_return = ctx
303 .ast_registry
304 .get(symbol_id)
305 .and_then(|item| match item {
306 PureItem::Fn(f) => Some(is_trailing_return(&f.body)),
307 _ => None,
308 })
309 .unwrap_or(false);
310
311 if has_trailing_return {
312 let fn_name = ctx
313 .registry
314 .path(symbol_id)
315 .map(|p| p.name().to_string())
316 .unwrap_or_else(|| "unknown".to_string());
317
318 let message = format!(
319 "Function `{}` has a redundant return at the end. Remove for idiomatic Rust.",
320 fn_name
321 );
322 (true, Some(message))
323 } else {
324 (false, None)
325 }
326 }
327
328 "RL090" => {
330 const STMT_THRESHOLD: usize = 30;
331
332 let stmt_count = ctx
333 .ast_registry
334 .get(symbol_id)
335 .and_then(|item| match item {
336 PureItem::Fn(f) => Some(count_statements(&f.body)),
337 _ => None,
338 })
339 .unwrap_or(0);
340
341 if stmt_count >= STMT_THRESHOLD {
342 let fn_name = ctx
343 .registry
344 .path(symbol_id)
345 .map(|p| p.name().to_string())
346 .unwrap_or_else(|| "unknown".to_string());
347
348 let message = format!(
349 "Function `{}` has {} statements (threshold: {}). Consider splitting.",
350 fn_name, stmt_count, STMT_THRESHOLD
351 );
352 (true, Some(message))
353 } else {
354 (false, None)
355 }
356 }
357
358 "RL091" => {
360 const PARAM_THRESHOLD: usize = 5;
361
362 let param_count = ctx
363 .ast_registry
364 .get(symbol_id)
365 .and_then(|item| match item {
366 PureItem::Fn(f) => Some(f.params.len()),
367 _ => None,
368 })
369 .unwrap_or(0);
370
371 if param_count >= PARAM_THRESHOLD {
372 let fn_name = ctx
373 .registry
374 .path(symbol_id)
375 .map(|p| p.name().to_string())
376 .unwrap_or_else(|| "unknown".to_string());
377
378 let message = format!(
379 "Function `{}` has {} parameters (threshold: {}). Consider grouping into a struct.",
380 fn_name, param_count, PARAM_THRESHOLD
381 );
382 (true, Some(message))
383 } else {
384 (false, None)
385 }
386 }
387
388 "RL004" => {
390 let unwrap_or_default_count = ctx
391 .ast_registry
392 .get(symbol_id)
393 .and_then(|item| match item {
394 PureItem::Fn(f) => Some(count_unwrap_or_default_calls(&f.body)),
395 _ => None,
396 })
397 .unwrap_or(0);
398
399 if unwrap_or_default_count > 0 {
400 let fn_name = ctx
401 .registry
402 .path(symbol_id)
403 .map(|p| p.name().to_string())
404 .unwrap_or_else(|| "unknown".to_string());
405
406 let message = format!(
407 "`{}` has {} unwrap_or(Default::default()) call{} that can be simplified to unwrap_or_default()",
408 fn_name,
409 unwrap_or_default_count,
410 if unwrap_or_default_count == 1 { "" } else { "s" }
411 );
412 (true, Some(message))
413 } else {
414 (false, None)
415 }
416 }
417
418 "RL040" => {
420 let collapsible_count = ctx
421 .ast_registry
422 .get(symbol_id)
423 .and_then(|item| match item {
424 PureItem::Fn(f) => Some(count_collapsible_ifs(&f.body)),
425 _ => None,
426 })
427 .unwrap_or(0);
428
429 if collapsible_count > 0 {
430 let fn_name = ctx
431 .registry
432 .path(symbol_id)
433 .map(|p| p.name().to_string())
434 .unwrap_or_else(|| "unknown".to_string());
435
436 let message = format!(
437 "Nested if statements may be collapsible in `{}` ({} occurrence{})",
438 fn_name,
439 collapsible_count,
440 if collapsible_count == 1 { "" } else { "s" }
441 );
442 (true, Some(message))
443 } else {
444 (false, None)
445 }
446 }
447
448 "RL050" => {
450 let empty_wild_count = ctx
451 .ast_registry
452 .get(symbol_id)
453 .and_then(|item| match item {
454 PureItem::Fn(f) => Some(count_empty_wildcard_arms(&f.body)),
455 _ => None,
456 })
457 .unwrap_or(0);
458
459 if empty_wild_count > 0 {
460 let fn_name = ctx
461 .registry
462 .path(symbol_id)
463 .map(|p| p.name().to_string())
464 .unwrap_or_else(|| "unknown".to_string());
465
466 let message = format!(
467 "`{}` has {} empty wildcard arm{} (`_ => {{}}`). Expand into explicit variant arms.",
468 fn_name,
469 empty_wild_count,
470 if empty_wild_count == 1 { "" } else { "s" }
471 );
472 (true, Some(message))
473 } else {
474 (false, None)
475 }
476 }
477
478 "RL070" => {
480 const MIN_VARIANTS: usize = 4;
481
482 let variant_count = ctx
483 .detail_store()
484 .enum_(symbol_id)
485 .map(|d| d.variants.len())
486 .unwrap_or(0);
487
488 if variant_count < MIN_VARIANTS {
489 return (false, None);
490 }
491
492 let impl_count = ctx.code_graph().impl_count(symbol_id);
494 if impl_count == 0 {
495 return (false, None);
496 }
497
498 let enum_name = ctx
499 .registry
500 .path(symbol_id)
501 .map(|p| p.name().to_string())
502 .unwrap_or_else(|| "unknown".to_string());
503
504 let message = format!(
505 "Enum `{}` has {} variants and {} impl block{} - consider EnumToTrait for extensibility",
506 enum_name,
507 variant_count,
508 impl_count,
509 if impl_count == 1 { "" } else { "s" }
510 );
511 (true, Some(message))
512 }
513
514 "RL003" => {
516 let func = match ctx.ast_registry.get(symbol_id) {
517 Some(PureItem::Fn(f)) => f,
518 _ => return (true, None),
519 };
520
521 let mut indexed_collections = Vec::new();
522 collect_indexed_collections(&func.body, &mut indexed_collections);
523
524 if indexed_collections.is_empty() {
525 return (false, None);
526 }
527
528 let mut len_aliases = std::collections::HashMap::new();
530 collect_len_aliases(&func.body, &mut len_aliases);
531
532 let mut len_checked_collections = Vec::new();
533 collect_len_checked_collections(
534 &func.body,
535 &mut len_checked_collections,
536 &len_aliases,
537 );
538
539 let all_guarded = indexed_collections
541 .iter()
542 .all(|coll| len_checked_collections.contains(coll));
543
544 if all_guarded {
545 (false, None)
546 } else {
547 let fn_name = ctx
548 .registry
549 .path(symbol_id)
550 .map(|p| p.name().to_string())
551 .unwrap_or_else(|| "unknown".to_string());
552
553 let unguarded: Vec<_> = indexed_collections
554 .iter()
555 .filter(|c| !len_checked_collections.contains(c))
556 .collect();
557
558 let message = format!(
559 "Function `{}` has unguarded index on: {}",
560 fn_name,
561 unguarded
562 .iter()
563 .map(|s| s.as_str())
564 .collect::<Vec<_>>()
565 .join(", "),
566 );
567 (true, Some(message))
568 }
569 }
570
571 _ => (true, None),
573 }
574 }
575
576 fn scan_body(&self, ctx: &AnalysisContext, symbol_id: SymbolId) -> Vec<MatchResult> {
578 if self.rule.id == "RL003" {
580 return self.scan_body_rl003(ctx, symbol_id);
581 }
582
583 let body_match = match &self.rule.query.body {
584 Some(b) => b,
585 None => return vec![], };
587
588 let func = match ctx.ast_registry.get(symbol_id) {
590 Some(PureItem::Fn(f)) => f,
591 _ => return vec![],
592 };
593
594 let mut results = Vec::new();
595
596 if let Some(ref patterns) = body_match.contains {
598 for pattern in patterns {
599 let scanner = BodyScanner::new(pattern);
600 let matches = scanner.scan_fn(func);
601 results.extend(matches);
602 }
603 }
604
605 if let Some(ref patterns) = body_match.all_of {
607 let all_matched = patterns.iter().all(|pattern| {
608 let scanner = BodyScanner::new(pattern);
609 !scanner.scan_fn(func).is_empty()
610 });
611 if !all_matched {
612 return vec![];
614 }
615 }
616
617 if let Some(ref patterns) = body_match.not_contains {
619 for pattern in patterns {
620 let scanner = BodyScanner::new(pattern);
621 if !scanner.scan_fn(func).is_empty() {
622 return vec![];
624 }
625 }
626 }
627
628 results
629 }
630
631 fn scan_body_rl003(&self, ctx: &AnalysisContext, symbol_id: SymbolId) -> Vec<MatchResult> {
633 let func = match ctx.ast_registry.get(symbol_id) {
634 Some(PureItem::Fn(f)) => f,
635 _ => return vec![],
636 };
637
638 let mut indexed = Vec::new();
639 collect_indexed_collections(&func.body, &mut indexed);
640 if indexed.is_empty() {
641 return vec![];
642 }
643
644 let mut aliases = std::collections::HashMap::new();
645 collect_len_aliases(&func.body, &mut aliases);
646
647 let mut checked = Vec::new();
648 collect_len_checked_collections(&func.body, &mut checked, &aliases);
649
650 collect_split_derived(&func.body, &mut checked);
652
653 let unguarded: Vec<String> = indexed
654 .into_iter()
655 .filter(|c| !checked.contains(c))
656 .collect();
657 if unguarded.is_empty() {
658 return vec![];
659 }
660
661 let mut results = Vec::new();
662 scan_unguarded_indexes_block(&func.body, &unguarded, &mut results);
663 results
664 }
665
666 fn interpolate_message(&self, match_result: &MatchResult) -> String {
668 let mut message = self.rule.message.clone();
669
670 for (var_name, captured) in &match_result.captures {
672 let placeholder = format!("{{{}.text}}", var_name);
673 message = message.replace(&placeholder, &captured.text);
674
675 message = message.replace(&format!("{{{}}}", var_name), &captured.text);
677 }
678
679 message
680 }
681
682 fn get_location(&self, ctx: &AnalysisContext, symbol_id: SymbolId) -> Option<SuggestLocation> {
684 SuggestLocation::from_context(ctx, symbol_id)
685 }
686
687 fn get_target_module_path(
691 &self,
692 ctx: &AnalysisContext,
693 opportunity: &SuggestOpportunity,
694 ) -> Option<SymbolPath> {
695 let symbol_id = opportunity.primary_target()?;
696 let symbol_path = ctx.registry.path(symbol_id)?;
697
698 symbol_path.parent()
700 }
701
702 fn generate_specs_for_rule(
708 &self,
709 ctx: &AnalysisContext,
710 opportunity: &SuggestOpportunity,
711 ) -> SuggestResult<Vec<MutationSpec>> {
712 let target_module = self.get_target_module_path(ctx, opportunity);
713 let target_fn = Some(opportunity.location.symbol_name().to_string());
714
715 if let Some(fix_json) = &self.rule.fix {
717 if let Ok(mut spec) = serde_json::from_value::<MutationSpec>(fix_json.clone()) {
718 Self::fill_mutation_spec_context(&mut spec, target_module, target_fn);
720 return Ok(vec![spec]);
721 }
722 }
723
724 match self.rule.id.as_str() {
726 "RL060" => {
729 let symbol_id = opportunity.primary_target();
730 if let Some(trait_id) = symbol_id {
731 let _trait_name = ctx
733 .registry
734 .path(trait_id)
735 .map(|p| p.name().to_string())
736 .unwrap_or_default();
737
738 let implementor = ctx.code_graph().implementors_of(trait_id).next();
740 let struct_name = implementor
741 .and_then(|id| ctx.registry.path(id))
742 .map(|p| p.name().to_string())
743 .unwrap_or_default();
744
745 if !struct_name.is_empty() {
746 Ok(vec![MutationSpec::InlineTrait {
747 target: ryo_executor::MutationTargetSymbol::ById(trait_id),
748 struct_name,
749 remove_trait: true,
750 }])
751 } else {
752 Ok(Vec::new())
753 }
754 } else {
755 Ok(Vec::new())
756 }
757 }
758
759 "RL061" => {
762 let symbol_id = opportunity.primary_target();
763 if let Some(impl_id) = symbol_id {
764 let struct_name = ctx
766 .detail_store()
767 .impl_(impl_id)
768 .map(|d| d.self_ty.clone())
769 .unwrap_or_default();
770
771 if !struct_name.is_empty() {
772 let trait_name = format!("{}Trait", struct_name);
774
775 Ok(vec![MutationSpec::ExtractTrait {
776 target: ryo_executor::MutationTargetSymbol::ById(impl_id),
777 trait_name,
778 methods: None, }])
780 } else {
781 Ok(Vec::new())
782 }
783 } else {
784 Ok(Vec::new())
785 }
786 }
787
788 "RL070" => {
790 let symbol_id = opportunity.primary_target();
791 if let Some(enum_id) = symbol_id {
792 self.generate_enum_to_trait_specs(ctx, enum_id)
793 } else {
794 Ok(Vec::new())
795 }
796 }
797
798 _ => Ok(Vec::new()),
800 }
801 }
802
803 fn fill_mutation_spec_context(
807 spec: &mut MutationSpec,
808 target_module: Option<SymbolPath>,
809 target_fn: Option<String>,
810 ) {
811 let _ = (spec, target_module, target_fn);
815 }
816
817 fn generate_enum_to_trait_specs(
826 &self,
827 _ctx: &AnalysisContext,
828 enum_id: SymbolId,
829 ) -> SuggestResult<Vec<MutationSpec>> {
830 Ok(vec![MutationSpec::EnumToTrait {
832 target: ryo_executor::MutationTargetSymbol::ById(enum_id),
833 trait_name: None, remove_enum: true,
835 strategy: EnumToTraitStrategy::default(),
836 match_handling: MatchHandling::default(),
837 }])
838 }
839}
840
841impl std::fmt::Debug for PatternBasedSuggest {
842 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
843 f.debug_struct("PatternBasedSuggest")
844 .field("rule_id", &self.rule.id)
845 .field("rule_name", &self.rule.name)
846 .finish()
847 }
848}
849
850impl Suggest for PatternBasedSuggest {
851 fn name(&self) -> &'static str {
852 self.name
853 }
854
855 fn description(&self) -> &str {
856 &self.description
857 }
858
859 fn category(&self) -> SuggestCategory {
860 SuggestCategory::Lint
861 }
862
863 fn safety_level(&self) -> SafetyLevel {
864 Self::to_safety_level(self.rule.severity)
865 }
866
867 fn priority_weight(&self) -> f32 {
868 match self.rule.severity {
869 PatternSeverity::Error => 1.0,
870 PatternSeverity::Warning => 0.8,
871 PatternSeverity::Info => 0.5,
872 PatternSeverity::Hint => 0.3,
873 }
874 }
875
876 fn rule_id(&self) -> Option<&str> {
877 Some(&self.rule.id)
878 }
879
880 fn detect(&self, ctx: &AnalysisContext, symbols: &[SymbolId]) -> Vec<SuggestOpportunity> {
881 let mut opportunities = Vec::new();
882 let mut next_id = 0u32;
883
884 let symbols_to_check: Box<dyn Iterator<Item = SymbolId>> = if symbols.is_empty() {
886 if let Some(kind) = &self.rule.query.kind {
888 use ryo_pattern::SymbolKind as PatternKind;
889 let symbol_kind = match kind {
890 PatternKind::Function => Some(SymbolKind::Function),
891 PatternKind::Struct => Some(SymbolKind::Struct),
892 PatternKind::Enum => Some(SymbolKind::Enum),
893 PatternKind::Trait => Some(SymbolKind::Trait),
894 PatternKind::Impl => Some(SymbolKind::Impl),
895 PatternKind::Mod => Some(SymbolKind::Mod),
896 _ => None,
897 };
898
899 if let Some(sk) = symbol_kind {
900 Box::new(ctx.registry.iter_by_kind(sk))
901 } else {
902 Box::new(ctx.registry.iter().map(|(id, _)| id))
903 }
904 } else {
905 Box::new(ctx.registry.iter().map(|(id, _)| id))
906 }
907 } else {
908 Box::new(symbols.iter().copied())
909 };
910
911 let binary_crates = if self.rule.scope.is_empty() {
913 None
914 } else {
915 Some(SymbolScope::binary_crate_names(ctx))
916 };
917
918 for symbol_id in symbols_to_check {
919 if let Some(ref bin_crates) = binary_crates {
921 let symbol_scope = SymbolScope::resolve(ctx, symbol_id, bin_crates);
922 let scope_str = symbol_scope.to_string();
923 if !self.rule.scope.iter().any(|s| s == &scope_str) {
924 continue;
925 }
926 }
927
928 if !self.matches_kind(ctx, symbol_id) {
930 continue;
931 }
932
933 if !self.matches_attrs(ctx, symbol_id) {
935 continue;
936 }
937
938 let (passes_dynamic, custom_message) = self.matches_dynamic_checks(ctx, symbol_id);
940 if !passes_dynamic {
941 continue;
942 }
943
944 let matches = self.scan_body(ctx, symbol_id);
946
947 if self.rule.query.body.is_some() && matches.is_empty() {
949 continue;
950 }
951
952 let Some(location) = self.get_location(ctx, symbol_id) else {
954 continue; };
956
957 if matches.is_empty() && self.rule.query.body.is_none() {
958 let message = custom_message
961 .clone()
962 .unwrap_or_else(|| self.rule.message.clone());
963 let opp = self.create_lint_opportunity(
964 OpportunityId::new(next_id),
965 vec![symbol_id],
966 location,
967 message,
968 LintDetails {
969 suggestion: self.rule.suggestion.clone(),
970 expected: None,
971 actual: None,
972 },
973 );
974 opportunities.push(opp);
975 next_id += 1;
976 } else if let Some(msg) = custom_message.clone() {
977 let opp = self.create_lint_opportunity(
981 OpportunityId::new(next_id),
982 vec![symbol_id],
983 location,
984 msg,
985 LintDetails {
986 suggestion: self.rule.suggestion.clone(),
987 expected: None,
988 actual: None,
989 },
990 );
991 opportunities.push(opp);
992 next_id += 1;
993 } else {
994 for match_result in matches {
996 let message = self.interpolate_message(&match_result);
997
998 let opp = self.create_lint_opportunity(
999 OpportunityId::new(next_id),
1000 vec![symbol_id],
1001 location.clone(),
1002 message,
1003 LintDetails {
1004 suggestion: self.rule.suggestion.clone(),
1005 expected: None,
1006 actual: None,
1007 },
1008 );
1009 opportunities.push(opp);
1010 next_id += 1;
1011 }
1012 }
1013 }
1014
1015 opportunities
1016 }
1017
1018 fn to_mutation_specs(
1019 &self,
1020 ctx: &AnalysisContext,
1021 opportunity: &SuggestOpportunity,
1022 ) -> SuggestResult<Vec<MutationSpec>> {
1023 self.generate_specs_for_rule(ctx, opportunity)
1025 }
1026}
1027
1028impl LintSuggest for PatternBasedSuggest {
1029 fn code(&self) -> &'static str {
1030 Box::leak(self.rule.id.clone().into_boxed_str())
1034 }
1035
1036 fn default_severity(&self) -> LintSeverity {
1037 Self::to_lint_severity(self.rule.severity)
1038 }
1039}
1040
1041fn count_statements(block: &PureBlock) -> usize {
1043 let mut count = 0;
1044 for stmt in &block.stmts {
1045 count += 1;
1046 match stmt {
1048 PureStmt::Expr(expr) | PureStmt::Semi(expr) => {
1049 count += count_statements_in_expr(expr);
1050 }
1051 _ => {}
1052 }
1053 }
1054 count
1055}
1056
1057fn is_trailing_return(block: &PureBlock) -> bool {
1059 match block.stmts.last() {
1060 Some(PureStmt::Semi(expr)) | Some(PureStmt::Expr(expr)) => {
1061 matches!(expr, ryo_source::pure::PureExpr::Return(Some(_)))
1062 }
1063 _ => false,
1064 }
1065}
1066
1067fn count_statements_in_expr(expr: &ryo_source::pure::PureExpr) -> usize {
1069 use ryo_source::pure::PureExpr;
1070 match expr {
1071 PureExpr::Block { block, .. } => count_statements(block),
1072 PureExpr::If {
1073 then_branch,
1074 else_branch,
1075 ..
1076 } => {
1077 let mut count = count_statements(then_branch);
1078 if let Some(else_expr) = else_branch {
1079 count += count_statements_in_expr(else_expr);
1080 }
1081 count
1082 }
1083 PureExpr::Match { arms, .. } => {
1084 arms.iter().map(|a| count_statements_in_expr(&a.body)).sum()
1085 }
1086 PureExpr::Loop { body: block, .. }
1087 | PureExpr::While { body: block, .. }
1088 | PureExpr::For { body: block, .. } => count_statements(block),
1089 _ => 0,
1090 }
1091}
1092
1093fn count_empty_wildcard_arms(block: &PureBlock) -> usize {
1095 let mut count = 0;
1096 for stmt in &block.stmts {
1097 let expr = match stmt {
1098 PureStmt::Expr(e) | PureStmt::Semi(e) => e,
1099 _ => continue,
1100 };
1101 count += count_empty_wildcard_arms_in_expr(expr);
1102 }
1103 count
1104}
1105
1106fn count_empty_wildcard_arms_in_expr(expr: &PureExpr) -> usize {
1107 let mut count = 0;
1108 match expr {
1109 PureExpr::Match { arms, .. } => {
1110 for arm in arms {
1112 if is_empty_wildcard_arm(arm) {
1113 count += 1;
1114 }
1115 count += count_empty_wildcard_arms_in_expr(&arm.body);
1117 }
1118 }
1119 PureExpr::If {
1120 then_branch,
1121 else_branch,
1122 ..
1123 } => {
1124 count += count_empty_wildcard_arms(then_branch);
1125 if let Some(else_expr) = else_branch {
1126 count += count_empty_wildcard_arms_in_expr(else_expr);
1127 }
1128 }
1129 PureExpr::Block { block, .. } => {
1130 count += count_empty_wildcard_arms(block);
1131 }
1132 PureExpr::Loop { body: block, .. }
1133 | PureExpr::While { body: block, .. }
1134 | PureExpr::For { body: block, .. } => {
1135 count += count_empty_wildcard_arms(block);
1136 }
1137 PureExpr::Closure { body, .. } => {
1138 count += count_empty_wildcard_arms_in_expr(body);
1139 }
1140 _ => {}
1141 }
1142 count
1143}
1144
1145fn is_empty_wildcard_arm(arm: &PureMatchArm) -> bool {
1147 if !matches!(arm.pattern, PurePattern::Wild) {
1149 return false;
1150 }
1151 if arm.guard.is_some() {
1153 return false;
1154 }
1155 match &arm.body {
1157 PureExpr::Tuple(items) if items.is_empty() => true,
1158 PureExpr::Block { block, .. } if block.stmts.is_empty() => true,
1159 PureExpr::Lit(s) if s == "()" => true,
1160 _ => false,
1161 }
1162}
1163
1164fn count_collapsible_ifs(block: &PureBlock) -> usize {
1171 let mut count = 0;
1172 for stmt in &block.stmts {
1173 let expr = match stmt {
1174 PureStmt::Expr(e) | PureStmt::Semi(e) => e,
1175 _ => continue,
1176 };
1177 count += count_collapsible_ifs_in_expr(expr);
1178 }
1179 count
1180}
1181
1182fn count_collapsible_ifs_in_expr(expr: &ryo_source::pure::PureExpr) -> usize {
1183 use ryo_source::pure::PureExpr;
1184 let mut count = 0;
1185
1186 match expr {
1187 PureExpr::If {
1188 cond: outer_cond,
1189 then_branch,
1190 else_branch: None,
1191 } => {
1192 if then_branch.stmts.len() == 1 && !matches!(outer_cond.as_ref(), PureExpr::Let { .. })
1195 {
1196 let inner = match &then_branch.stmts[0] {
1197 PureStmt::Expr(e) | PureStmt::Semi(e) => Some(e),
1198 _ => None,
1199 };
1200 if let Some(PureExpr::If {
1201 cond: inner_cond,
1202 else_branch: None,
1203 ..
1204 }) = inner
1205 {
1206 if !matches!(inner_cond.as_ref(), PureExpr::Let { .. }) {
1208 count += 1;
1209 }
1210 }
1211 }
1212 count += count_collapsible_ifs(then_branch);
1214 }
1215 PureExpr::If {
1216 then_branch,
1217 else_branch: Some(else_expr),
1218 ..
1219 } => {
1220 count += count_collapsible_ifs(then_branch);
1221 count += count_collapsible_ifs_in_expr(else_expr);
1222 }
1223 PureExpr::Block { block, .. } => {
1224 count += count_collapsible_ifs(block);
1225 }
1226 PureExpr::Match { arms, .. } => {
1227 for arm in arms {
1228 count += count_collapsible_ifs_in_expr(&arm.body);
1229 }
1230 }
1231 PureExpr::Loop { body: block, .. }
1232 | PureExpr::While { body: block, .. }
1233 | PureExpr::For { body: block, .. } => {
1234 count += count_collapsible_ifs(block);
1235 }
1236 PureExpr::Closure { body, .. } => {
1237 count += count_collapsible_ifs_in_expr(body);
1238 }
1239 _ => {}
1240 }
1241 count
1242}
1243
1244fn count_unwrap_or_default_calls(block: &PureBlock) -> usize {
1246 let mut count = 0;
1247 for stmt in &block.stmts {
1248 let expr = match stmt {
1249 PureStmt::Expr(e) | PureStmt::Semi(e) => e,
1250 PureStmt::Local { init: Some(e), .. } => e,
1251 _ => continue,
1252 };
1253 count += count_unwrap_or_default_calls_in_expr(expr);
1254 }
1255 count
1256}
1257
1258fn count_unwrap_or_default_calls_in_expr(expr: &PureExpr) -> usize {
1259 let mut count = 0;
1260 match expr {
1261 PureExpr::MethodCall {
1262 receiver,
1263 method,
1264 args,
1265 ..
1266 } => {
1267 if method == "unwrap_or" && args.len() == 1 && is_default_default_call(&args[0]) {
1268 count += 1;
1269 }
1270 count += count_unwrap_or_default_calls_in_expr(receiver);
1271 for arg in args {
1272 count += count_unwrap_or_default_calls_in_expr(arg);
1273 }
1274 }
1275 PureExpr::Call { func, args } => {
1276 count += count_unwrap_or_default_calls_in_expr(func);
1277 for arg in args {
1278 count += count_unwrap_or_default_calls_in_expr(arg);
1279 }
1280 }
1281 PureExpr::Binary { left, right, .. } => {
1282 count += count_unwrap_or_default_calls_in_expr(left);
1283 count += count_unwrap_or_default_calls_in_expr(right);
1284 }
1285 PureExpr::Unary { expr, .. } | PureExpr::Try(expr) | PureExpr::Await(expr) => {
1286 count += count_unwrap_or_default_calls_in_expr(expr);
1287 }
1288 PureExpr::Field { expr, .. } => {
1289 count += count_unwrap_or_default_calls_in_expr(expr);
1290 }
1291 PureExpr::Index { expr, index } => {
1292 count += count_unwrap_or_default_calls_in_expr(expr);
1293 count += count_unwrap_or_default_calls_in_expr(index);
1294 }
1295 PureExpr::If {
1296 cond,
1297 then_branch,
1298 else_branch,
1299 } => {
1300 count += count_unwrap_or_default_calls_in_expr(cond);
1301 count += count_unwrap_or_default_calls(then_branch);
1302 if let Some(else_expr) = else_branch {
1303 count += count_unwrap_or_default_calls_in_expr(else_expr);
1304 }
1305 }
1306 PureExpr::Match { expr, arms } => {
1307 count += count_unwrap_or_default_calls_in_expr(expr);
1308 for arm in arms {
1309 count += count_unwrap_or_default_calls_in_expr(&arm.body);
1310 }
1311 }
1312 PureExpr::Block { block, .. } => {
1313 count += count_unwrap_or_default_calls(block);
1314 }
1315 PureExpr::Loop { body: block, .. }
1316 | PureExpr::While { body: block, .. }
1317 | PureExpr::For { body: block, .. } => {
1318 count += count_unwrap_or_default_calls(block);
1319 }
1320 PureExpr::Closure { body, .. } => {
1321 count += count_unwrap_or_default_calls_in_expr(body);
1322 }
1323 _ => {}
1324 }
1325 count
1326}
1327
1328fn is_default_default_call(expr: &PureExpr) -> bool {
1330 match expr {
1331 PureExpr::Call { func, args } if args.is_empty() => {
1332 matches!(func.as_ref(), PureExpr::Path(p) if p == "Default::default" || p == "Default :: default")
1333 }
1334 _ => false,
1335 }
1336}
1337
1338fn expr_root_name(expr: &PureExpr) -> Option<&str> {
1340 match expr {
1341 PureExpr::Path(name) => Some(name.as_str()),
1342 PureExpr::Field { expr, .. } => expr_root_name(expr),
1343 PureExpr::MethodCall { receiver, .. } => expr_root_name(receiver),
1344 _ => None,
1345 }
1346}
1347
1348fn scan_unguarded_indexes_block(
1350 block: &PureBlock,
1351 unguarded: &[String],
1352 results: &mut Vec<MatchResult>,
1353) {
1354 for stmt in &block.stmts {
1355 match stmt {
1356 PureStmt::Local {
1357 init: Some(expr), ..
1358 } => {
1359 scan_unguarded_indexes_expr(expr, unguarded, results);
1360 }
1361 PureStmt::Semi(expr) | PureStmt::Expr(expr) => {
1362 scan_unguarded_indexes_expr(expr, unguarded, results);
1363 }
1364 _ => {}
1365 }
1366 }
1367}
1368
1369fn scan_unguarded_indexes_expr(
1370 expr: &PureExpr,
1371 unguarded: &[String],
1372 results: &mut Vec<MatchResult>,
1373) {
1374 if let PureExpr::Index { expr: coll, index } = expr {
1376 if let Some(name) = expr_root_name(coll) {
1377 if unguarded.iter().any(|u| u == name) {
1378 results.push(MatchResult::matched());
1379 }
1380 }
1381 scan_unguarded_indexes_expr(coll, unguarded, results);
1382 scan_unguarded_indexes_expr(index, unguarded, results);
1383 return;
1384 }
1385
1386 match expr {
1387 PureExpr::Binary { left, right, .. } => {
1388 scan_unguarded_indexes_expr(left, unguarded, results);
1389 scan_unguarded_indexes_expr(right, unguarded, results);
1390 }
1391 PureExpr::Unary { expr, .. } | PureExpr::Try(expr) | PureExpr::Field { expr, .. } => {
1392 scan_unguarded_indexes_expr(expr, unguarded, results);
1393 }
1394 PureExpr::MethodCall { receiver, args, .. } => {
1395 scan_unguarded_indexes_expr(receiver, unguarded, results);
1396 for a in args {
1397 scan_unguarded_indexes_expr(a, unguarded, results);
1398 }
1399 }
1400 PureExpr::Call { func, args } => {
1401 scan_unguarded_indexes_expr(func, unguarded, results);
1402 for a in args {
1403 scan_unguarded_indexes_expr(a, unguarded, results);
1404 }
1405 }
1406 PureExpr::If {
1407 cond,
1408 then_branch,
1409 else_branch,
1410 ..
1411 } => {
1412 scan_unguarded_indexes_expr(cond, unguarded, results);
1413 scan_unguarded_indexes_block(then_branch, unguarded, results);
1414 if let Some(e) = else_branch {
1415 scan_unguarded_indexes_expr(e, unguarded, results);
1416 }
1417 }
1418 PureExpr::Block { block, .. } => {
1419 scan_unguarded_indexes_block(block, unguarded, results);
1420 }
1421 PureExpr::Match { expr: e, arms } => {
1422 scan_unguarded_indexes_expr(e, unguarded, results);
1423 for arm in arms {
1424 scan_unguarded_indexes_expr(&arm.body, unguarded, results);
1425 }
1426 }
1427 PureExpr::Loop { body, .. } => {
1428 scan_unguarded_indexes_block(body, unguarded, results);
1429 }
1430 PureExpr::While { cond, body, .. } => {
1431 scan_unguarded_indexes_expr(cond, unguarded, results);
1432 scan_unguarded_indexes_block(body, unguarded, results);
1433 }
1434 PureExpr::For {
1435 expr: iter, body, ..
1436 } => {
1437 scan_unguarded_indexes_expr(iter, unguarded, results);
1438 scan_unguarded_indexes_block(body, unguarded, results);
1439 }
1440 PureExpr::Closure { body, .. } => {
1441 scan_unguarded_indexes_expr(body, unguarded, results);
1442 }
1443 PureExpr::Tuple(items) | PureExpr::Array(items) => {
1444 for item in items {
1445 scan_unguarded_indexes_expr(item, unguarded, results);
1446 }
1447 }
1448 PureExpr::Let { expr, .. } | PureExpr::Return(Some(expr)) => {
1449 scan_unguarded_indexes_expr(expr, unguarded, results);
1450 }
1451 _ => {}
1452 }
1453}
1454
1455fn collect_indexed_collections(block: &PureBlock, out: &mut Vec<String>) {
1457 for stmt in &block.stmts {
1458 let expr = match stmt {
1459 PureStmt::Expr(e) | PureStmt::Semi(e) => e,
1460 PureStmt::Local { init, .. } => {
1461 if let Some(init) = init {
1462 collect_indexed_collections_in_expr(init, out);
1463 }
1464 continue;
1465 }
1466 _ => continue,
1467 };
1468 collect_indexed_collections_in_expr(expr, out);
1469 }
1470}
1471
1472fn collect_indexed_collections_in_expr(expr: &PureExpr, out: &mut Vec<String>) {
1473 match expr {
1474 PureExpr::Index { expr: coll, index } => {
1475 if let Some(name) = expr_root_name(coll) {
1476 if !out.contains(&name.to_string()) {
1477 out.push(name.to_string());
1478 }
1479 }
1480 collect_indexed_collections_in_expr(coll, out);
1481 collect_indexed_collections_in_expr(index, out);
1482 }
1483 PureExpr::Binary { left, right, .. } => {
1484 collect_indexed_collections_in_expr(left, out);
1485 collect_indexed_collections_in_expr(right, out);
1486 }
1487 PureExpr::Unary { expr, .. } | PureExpr::Try(expr) | PureExpr::Return(Some(expr)) => {
1488 collect_indexed_collections_in_expr(expr, out);
1489 }
1490 PureExpr::Call { func, args } => {
1491 collect_indexed_collections_in_expr(func, out);
1492 for a in args {
1493 collect_indexed_collections_in_expr(a, out);
1494 }
1495 }
1496 PureExpr::MethodCall { receiver, args, .. } => {
1497 collect_indexed_collections_in_expr(receiver, out);
1498 for a in args {
1499 collect_indexed_collections_in_expr(a, out);
1500 }
1501 }
1502 PureExpr::Field { expr, .. } => {
1503 collect_indexed_collections_in_expr(expr, out);
1504 }
1505 PureExpr::If {
1506 cond,
1507 then_branch,
1508 else_branch,
1509 ..
1510 } => {
1511 collect_indexed_collections_in_expr(cond, out);
1512 collect_indexed_collections(then_branch, out);
1513 if let Some(e) = else_branch {
1514 collect_indexed_collections_in_expr(e, out);
1515 }
1516 }
1517 PureExpr::Block { block, .. } => {
1518 collect_indexed_collections(block, out);
1519 }
1520 PureExpr::Match { expr, arms } => {
1521 collect_indexed_collections_in_expr(expr, out);
1522 for arm in arms {
1523 collect_indexed_collections_in_expr(&arm.body, out);
1524 }
1525 }
1526 PureExpr::Loop { body, .. } | PureExpr::While { body, .. } | PureExpr::For { body, .. } => {
1527 collect_indexed_collections(body, out);
1528 }
1529 PureExpr::Closure { body, .. } => {
1530 collect_indexed_collections_in_expr(body, out);
1531 }
1532 PureExpr::Tuple(items) | PureExpr::Array(items) => {
1533 for item in items {
1534 collect_indexed_collections_in_expr(item, out);
1535 }
1536 }
1537 PureExpr::Let { expr, .. } => {
1538 collect_indexed_collections_in_expr(expr, out);
1539 }
1540 _ => {}
1541 }
1542}
1543
1544fn collect_len_aliases(block: &PureBlock, aliases: &mut std::collections::HashMap<String, String>) {
1546 for stmt in &block.stmts {
1547 if let PureStmt::Local { pattern, init, .. } = stmt {
1548 if let (
1549 PurePattern::Ident { name: var_name, .. },
1550 Some(PureExpr::MethodCall {
1551 receiver, method, ..
1552 }),
1553 ) = (pattern, init)
1554 {
1555 if matches!(method.as_str(), "len" | "find" | "rfind" | "position") {
1556 if let Some(coll_name) = expr_root_name(receiver) {
1557 aliases.insert(var_name.clone(), coll_name.to_string());
1558 }
1559 }
1560 }
1561 }
1562 if let Some(expr) = stmt.get_expr() {
1564 collect_len_aliases_in_expr(expr, aliases);
1565 }
1566 }
1567}
1568
1569fn collect_len_aliases_in_expr(
1570 expr: &PureExpr,
1571 aliases: &mut std::collections::HashMap<String, String>,
1572) {
1573 match expr {
1574 PureExpr::If {
1575 then_branch,
1576 else_branch,
1577 ..
1578 } => {
1579 collect_len_aliases(then_branch, aliases);
1580 if let Some(e) = else_branch {
1581 collect_len_aliases_in_expr(e, aliases);
1582 }
1583 }
1584 PureExpr::Block { block, .. } => collect_len_aliases(block, aliases),
1585 PureExpr::Loop { body, .. } | PureExpr::While { body, .. } | PureExpr::For { body, .. } => {
1586 collect_len_aliases(body, aliases);
1587 }
1588 PureExpr::Match { arms, .. } => {
1589 for arm in arms {
1590 collect_len_aliases_in_expr(&arm.body, aliases);
1591 }
1592 }
1593 PureExpr::Closure { body, .. } => collect_len_aliases_in_expr(body, aliases),
1594 _ => {}
1595 }
1596}
1597
1598fn collect_len_checked_collections(
1600 block: &PureBlock,
1601 out: &mut Vec<String>,
1602 aliases: &std::collections::HashMap<String, String>,
1603) {
1604 for stmt in &block.stmts {
1605 let expr = match stmt {
1606 PureStmt::Expr(e) | PureStmt::Semi(e) => e,
1607 PureStmt::Local { init, .. } => {
1608 if let Some(init) = init {
1609 collect_len_checked_in_expr(init, out, aliases);
1610 }
1611 continue;
1612 }
1613 _ => continue,
1614 };
1615 collect_len_checked_in_expr(expr, out, aliases);
1616 }
1617}
1618
1619fn collect_len_checked_in_expr(
1620 expr: &PureExpr,
1621 out: &mut Vec<String>,
1622 aliases: &std::collections::HashMap<String, String>,
1623) {
1624 match expr {
1625 PureExpr::If {
1626 cond,
1627 then_branch,
1628 else_branch,
1629 ..
1630 } => {
1631 collect_len_names_from_cond(cond, out, aliases);
1632 collect_len_checked_collections(then_branch, out, aliases);
1633 if let Some(e) = else_branch {
1634 collect_len_checked_in_expr(e, out, aliases);
1635 }
1636 }
1637 PureExpr::While { cond, body, .. } => {
1638 collect_len_names_from_cond(cond, out, aliases);
1639 collect_len_checked_collections(body, out, aliases);
1640 }
1641 PureExpr::For { expr, body, .. } => {
1642 if let PureExpr::Range { end: Some(end), .. } = expr.as_ref() {
1644 if let Some(name) = extract_bounds_receiver(end) {
1645 if !out.contains(&name) {
1646 out.push(name);
1647 }
1648 }
1649 if let PureExpr::Path(var) = end.as_ref() {
1651 if let Some(coll) = aliases.get(var.as_str()) {
1652 if !out.contains(coll) {
1653 out.push(coll.clone());
1654 }
1655 }
1656 }
1657 }
1658 collect_len_checked_collections(body, out, aliases);
1659 }
1660 PureExpr::Block { block, .. } => {
1661 collect_len_checked_collections(block, out, aliases);
1662 }
1663 PureExpr::Loop { body, .. } => {
1664 collect_len_checked_collections(body, out, aliases);
1665 }
1666 PureExpr::Match { expr, arms } => {
1667 if let Some(name) = extract_bounds_receiver(expr) {
1669 if !out.contains(&name) {
1670 out.push(name);
1671 }
1672 }
1673 if let PureExpr::Path(var) = expr.as_ref() {
1675 if let Some(coll) = aliases.get(var.as_str()) {
1676 if !out.contains(coll) {
1677 out.push(coll.clone());
1678 }
1679 }
1680 }
1681 if let PureExpr::Tuple(items) = expr.as_ref() {
1684 for item in items {
1685 if let Some(name) = extract_bounds_receiver(item) {
1686 if !out.contains(&name) {
1687 out.push(name);
1688 }
1689 }
1690 if let PureExpr::Path(var) = item {
1691 if let Some(coll) = aliases.get(var.as_str()) {
1692 if !out.contains(coll) {
1693 out.push(coll.clone());
1694 }
1695 }
1696 }
1697 }
1698 }
1699 for arm in arms {
1700 collect_len_checked_in_expr(&arm.body, out, aliases);
1701 }
1702 }
1703 PureExpr::Closure { body, .. } => {
1704 collect_len_checked_in_expr(body, out, aliases);
1705 }
1706 PureExpr::MethodCall { receiver, args, .. } => {
1708 collect_len_checked_in_expr(receiver, out, aliases);
1709 for a in args {
1710 collect_len_checked_in_expr(a, out, aliases);
1711 }
1712 }
1713 PureExpr::Call { func, args } => {
1714 collect_len_checked_in_expr(func, out, aliases);
1715 for a in args {
1716 collect_len_checked_in_expr(a, out, aliases);
1717 }
1718 }
1719 PureExpr::Binary { left, right, .. } => {
1720 collect_len_checked_in_expr(left, out, aliases);
1721 collect_len_checked_in_expr(right, out, aliases);
1722 }
1723 PureExpr::Unary { expr, .. } | PureExpr::Try(expr) | PureExpr::Field { expr, .. } => {
1724 collect_len_checked_in_expr(expr, out, aliases);
1725 }
1726 PureExpr::Tuple(items) | PureExpr::Array(items) => {
1727 for item in items {
1728 collect_len_checked_in_expr(item, out, aliases);
1729 }
1730 }
1731 PureExpr::Index { expr, index } => {
1732 collect_len_checked_in_expr(expr, out, aliases);
1733 collect_len_checked_in_expr(index, out, aliases);
1734 }
1735 PureExpr::Let { expr, .. } | PureExpr::Return(Some(expr)) => {
1736 collect_len_checked_in_expr(expr, out, aliases);
1737 }
1738 _ => {}
1739 }
1740}
1741
1742fn collect_len_names_from_cond(
1746 cond: &PureExpr,
1747 out: &mut Vec<String>,
1748 aliases: &std::collections::HashMap<String, String>,
1749) {
1750 match cond {
1751 PureExpr::Binary { op, left, right } => match op.as_str() {
1752 "<" | "<=" | ">" | ">=" | "==" | "!=" => {
1753 for side in [left.as_ref(), right.as_ref()] {
1755 if let Some(name) = extract_bounds_receiver(side) {
1756 if !out.contains(&name) {
1757 out.push(name);
1758 }
1759 }
1760 if let PureExpr::Path(var) = side {
1762 if let Some(coll) = aliases.get(var.as_str()) {
1763 if !out.contains(coll) {
1764 out.push(coll.clone());
1765 }
1766 }
1767 }
1768 }
1769 }
1770 "&&" | "||" => {
1771 collect_len_names_from_cond(left, out, aliases);
1772 collect_len_names_from_cond(right, out, aliases);
1773 }
1774 _ => {}
1775 },
1776 PureExpr::Unary { op, expr } if op == "!" => {
1777 collect_len_names_from_cond(expr, out, aliases);
1778 }
1779 PureExpr::MethodCall {
1781 receiver, method, ..
1782 } if is_bounds_implying_method(method) => {
1783 if let Some(name) = expr_root_name(receiver) {
1784 if !out.contains(&name.to_string()) {
1785 out.push(name.to_string());
1786 }
1787 }
1788 }
1789 PureExpr::Let { expr, .. } => {
1792 if let Some(name) = extract_bounds_receiver(expr) {
1793 if !out.contains(&name) {
1794 out.push(name);
1795 }
1796 }
1797 if let PureExpr::Path(var) = expr.as_ref() {
1799 if let Some(coll) = aliases.get(var.as_str()) {
1800 if !out.contains(coll) {
1801 out.push(coll.clone());
1802 }
1803 }
1804 }
1805 }
1806 _ => {}
1807 }
1808}
1809
1810fn is_bounds_implying_method(method: &str) -> bool {
1812 matches!(
1813 method,
1814 "is_empty"
1815 | "len"
1816 | "starts_with"
1817 | "ends_with"
1818 | "contains"
1819 | "find"
1820 | "rfind"
1821 | "position"
1822 | "get"
1823 | "get_mut"
1824 )
1825}
1826
1827fn extract_bounds_receiver(expr: &PureExpr) -> Option<String> {
1829 if let PureExpr::MethodCall {
1830 receiver, method, ..
1831 } = expr
1832 {
1833 if is_bounds_implying_method(method) {
1834 return expr_root_name(receiver).map(|s| s.to_string());
1835 }
1836 if matches!(
1838 method.as_str(),
1839 "saturating_sub" | "checked_sub" | "wrapping_sub"
1840 ) {
1841 return extract_bounds_receiver(receiver);
1842 }
1843 if matches!(
1845 method.as_str(),
1846 "or_else" | "and_then" | "map" | "unwrap_or" | "unwrap_or_else"
1847 ) {
1848 return extract_bounds_receiver(receiver);
1849 }
1850 }
1851 None
1852}
1853
1854fn collect_split_derived(block: &PureBlock, out: &mut Vec<String>) {
1859 for stmt in &block.stmts {
1860 if let PureStmt::Local { pattern, init, .. } = stmt {
1861 if let (PurePattern::Ident { name, .. }, Some(init_expr)) = (pattern, init) {
1862 if is_split_collect_chain(init_expr) && !out.contains(name) {
1863 out.push(name.clone());
1864 }
1865 }
1866 }
1867 }
1868}
1869
1870fn is_split_collect_chain(expr: &PureExpr) -> bool {
1872 if let PureExpr::MethodCall {
1873 receiver, method, ..
1874 } = expr
1875 {
1876 if method == "collect" {
1877 return has_split_in_chain(receiver);
1878 }
1879 }
1880 false
1881}
1882
1883fn has_split_in_chain(expr: &PureExpr) -> bool {
1884 if let PureExpr::MethodCall {
1885 receiver, method, ..
1886 } = expr
1887 {
1888 if matches!(
1889 method.as_str(),
1890 "split" | "splitn" | "split_whitespace" | "split_terminator" | "split_ascii_whitespace"
1891 ) {
1892 return true;
1893 }
1894 return has_split_in_chain(receiver);
1895 }
1896 false
1897}
1898
1899#[cfg(test)]
1900mod tests {
1901 use super::*;
1902 use ryo_pattern::{PatternQuery, Severity, SymbolKind as PatternKind};
1903
1904 fn create_test_rule() -> Rule {
1905 Rule::new(
1906 "TEST001",
1907 "test-rule",
1908 Severity::Warning,
1909 PatternQuery::new().kind(PatternKind::Function),
1910 "Test message",
1911 )
1912 }
1913
1914 #[test]
1915 fn test_pattern_based_suggest_basic() {
1916 let rule = create_test_rule();
1917 let suggest = PatternBasedSuggest::new(rule);
1918
1919 assert_eq!(suggest.name(), "TEST001"); assert_eq!(suggest.description(), "test-rule");
1921 assert_eq!(suggest.category(), SuggestCategory::Lint);
1922 assert_eq!(suggest.safety_level(), SafetyLevel::Confirm);
1923 }
1924
1925 #[test]
1926 fn test_severity_conversion() {
1927 assert_eq!(
1928 PatternBasedSuggest::to_lint_severity(Severity::Error),
1929 LintSeverity::Error
1930 );
1931 assert_eq!(
1932 PatternBasedSuggest::to_lint_severity(Severity::Warning),
1933 LintSeverity::Warning
1934 );
1935 assert_eq!(
1936 PatternBasedSuggest::to_lint_severity(Severity::Info),
1937 LintSeverity::Info
1938 );
1939 assert_eq!(
1940 PatternBasedSuggest::to_lint_severity(Severity::Hint),
1941 LintSeverity::Info
1942 );
1943 }
1944
1945 #[test]
1946 fn test_safety_level_conversion() {
1947 assert_eq!(
1948 PatternBasedSuggest::to_safety_level(Severity::Error),
1949 SafetyLevel::Manual
1950 );
1951 assert_eq!(
1952 PatternBasedSuggest::to_safety_level(Severity::Warning),
1953 SafetyLevel::Confirm
1954 );
1955 assert_eq!(
1956 PatternBasedSuggest::to_safety_level(Severity::Info),
1957 SafetyLevel::Auto
1958 );
1959 }
1960
1961 #[test]
1962 fn test_glob_matching() {
1963 let suggest = PatternBasedSuggest::new(create_test_rule());
1964
1965 assert!(suggest.matches_glob("*", "anything"));
1966 assert!(suggest.matches_glob("test_*", "test_foo"));
1967 assert!(!suggest.matches_glob("test_*", "foo_test"));
1968 assert!(suggest.matches_glob("*_test", "foo_test"));
1969 assert!(!suggest.matches_glob("*_test", "test_foo"));
1970 assert!(suggest.matches_glob("exact", "exact"));
1971 assert!(!suggest.matches_glob("exact", "different"));
1972 }
1973
1974 #[test]
1975 fn test_mutation_spec_generation_rl001() {
1976 let rule = Rule::new(
1978 "RL001",
1979 "no-unwrap-in-public",
1980 Severity::Warning,
1981 PatternQuery::new().kind(PatternKind::Function),
1982 "Avoid unwrap() in public function",
1983 );
1984 let suggest = PatternBasedSuggest::new(rule);
1985
1986 assert_eq!(suggest.rule.id, "RL001");
1988
1989 assert!(suggest.rule.id.starts_with("RL001"));
1992 }
1993
1994 #[test]
1995 fn test_mutation_spec_generation_rl020() {
1996 let rule = Rule::new(
1998 "RL020",
1999 "no-dbg-macro",
2000 Severity::Warning,
2001 PatternQuery::new().kind(PatternKind::Function),
2002 "Found dbg!() macro",
2003 );
2004 let suggest = PatternBasedSuggest::new(rule);
2005
2006 assert_eq!(suggest.rule.id, "RL020");
2007 }
2008
2009 #[test]
2010 fn test_mutation_spec_generation_unknown_rule() {
2011 let rule = Rule::new(
2013 "UNKNOWN",
2014 "unknown-rule",
2015 Severity::Info,
2016 PatternQuery::new(),
2017 "Unknown",
2018 );
2019 let suggest = PatternBasedSuggest::new(rule);
2020
2021 assert_eq!(suggest.rule.id, "UNKNOWN");
2023 }
2025
2026 #[test]
2027 fn test_priority_weight() {
2028 let error_rule = Rule::new(
2029 "E001",
2030 "error-rule",
2031 Severity::Error,
2032 PatternQuery::new(),
2033 "Error",
2034 );
2035 let warning_rule = Rule::new(
2036 "W001",
2037 "warning-rule",
2038 Severity::Warning,
2039 PatternQuery::new(),
2040 "Warning",
2041 );
2042 let info_rule = Rule::new(
2043 "I001",
2044 "info-rule",
2045 Severity::Info,
2046 PatternQuery::new(),
2047 "Info",
2048 );
2049
2050 assert_eq!(PatternBasedSuggest::new(error_rule).priority_weight(), 1.0);
2051 assert_eq!(
2052 PatternBasedSuggest::new(warning_rule).priority_weight(),
2053 0.8
2054 );
2055 assert_eq!(PatternBasedSuggest::new(info_rule).priority_weight(), 0.5);
2056 }
2057}