codelens_engine/
redundant_definitions.rs1use crate::project::{collect_files, ProjectRoot};
12use anyhow::Result;
13use regex::Regex;
14use serde::Serialize;
15use std::path::Path;
16use std::sync::LazyLock;
17
18static RUST_ONE_LINE_WRAPPER_RE: LazyLock<Regex> = LazyLock::new(|| {
24 Regex::new(
26 r#"(?m)^\s*(?:pub(?:\([^)]*\))?\s+)?fn\s+(?P<wrapper>[A-Za-z_][A-Za-z0-9_]*)\s*\([^)]*\)\s*(?:->\s*[^{]+?)?\s*\{\s*(?:self\.|Self::)?(?P<target>[A-Za-z_][A-Za-z0-9_]*)\s*\([^)]*?(?P<default>None|Default::default\(\)|true|false|-?\d+|"[^"]*")?\s*\)\s*;?\s*\}\s*$"#
27 ).unwrap()
28});
29
30#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
31pub struct RedundantDefinitionEntry {
32 pub file: String,
33 pub wrapper: String,
34 pub target: String,
35 pub line: usize,
36 pub default_arg: Option<String>,
37 pub kind: &'static str,
38}
39
40pub fn find_redundant_definitions(
46 project: &ProjectRoot,
47 max_results: usize,
48) -> Result<Vec<RedundantDefinitionEntry>> {
49 let mut results: Vec<RedundantDefinitionEntry> = Vec::new();
50 let candidates = collect_files(project.as_path(), is_rust_file)?;
51
52 for path in &candidates {
53 let source = match std::fs::read_to_string(path) {
54 Ok(s) => s,
55 Err(_) => continue,
56 };
57 let relative = project.to_relative(path);
58 scan_rust_source(&source, &relative, &mut results);
59 if max_results > 0 && results.len() >= max_results {
60 break;
61 }
62 }
63
64 results.sort_by(|a, b| {
65 a.target
66 .cmp(&b.target)
67 .then(a.file.cmp(&b.file))
68 .then(a.line.cmp(&b.line))
69 });
70 if max_results > 0 && results.len() > max_results {
71 results.truncate(max_results);
72 }
73 Ok(results)
74}
75
76fn scan_rust_source(source: &str, file: &str, out: &mut Vec<RedundantDefinitionEntry>) {
77 for m in RUST_ONE_LINE_WRAPPER_RE.captures_iter(source) {
78 let wrapper = m
79 .name("wrapper")
80 .map(|m| m.as_str().to_owned())
81 .unwrap_or_default();
82 let target = m
83 .name("target")
84 .map(|m| m.as_str().to_owned())
85 .unwrap_or_default();
86 if wrapper.is_empty() || target.is_empty() || wrapper == target {
87 continue;
88 }
89 let default_arg = m.name("default").map(|m| m.as_str().to_owned());
90 let line = byte_offset_to_line(source, m.get(0).map(|m| m.start()).unwrap_or(0));
91 out.push(RedundantDefinitionEntry {
92 file: file.to_owned(),
93 wrapper,
94 target,
95 line,
96 default_arg,
97 kind: "rust_one_line_wrapper",
98 });
99 }
100}
101
102fn byte_offset_to_line(source: &str, offset: usize) -> usize {
103 source[..offset.min(source.len())].matches('\n').count() + 1
104}
105
106fn is_rust_file(path: &Path) -> bool {
107 path.extension().and_then(|s| s.to_str()) == Some("rs")
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn detects_self_dot_wrapper_with_none_default() {
116 let source = r#"
117impl Foo {
118 pub fn record_x(&self) { self.record_x_for_session(None) }
119 pub fn record_y(&self) { self.record_y_for_session(None); }
120}
121 "#;
122 let mut out = Vec::new();
123 scan_rust_source(source, "telemetry.rs", &mut out);
124 assert_eq!(out.len(), 2, "got: {:?}", out);
125 assert_eq!(out[0].wrapper, "record_x");
126 assert_eq!(out[0].target, "record_x_for_session");
127 assert_eq!(out[0].default_arg.as_deref(), Some("None"));
128 assert_eq!(out[1].wrapper, "record_y");
129 }
130
131 #[test]
132 fn detects_bare_function_wrapper() {
133 let source = r#"
134pub fn helper(x: u32) -> bool { inner(x, false) }
135 "#;
136 let mut out = Vec::new();
137 scan_rust_source(source, "lib.rs", &mut out);
138 assert_eq!(out.len(), 1);
139 assert_eq!(out[0].wrapper, "helper");
140 assert_eq!(out[0].target, "inner");
141 assert_eq!(out[0].default_arg.as_deref(), Some("false"));
142 }
143
144 #[test]
145 fn skips_self_recursive_call() {
146 let source = r#"
147pub fn loop_me(&self) { self.loop_me(0) }
148 "#;
149 let mut out = Vec::new();
150 scan_rust_source(source, "x.rs", &mut out);
151 assert!(out.is_empty(), "got: {:?}", out);
153 }
154
155 #[test]
156 fn skips_multi_statement_body() {
157 let source = r#"
158pub fn complex(&self) {
159 let x = 1;
160 self.do_thing(x, None);
161}
162 "#;
163 let mut out = Vec::new();
164 scan_rust_source(source, "x.rs", &mut out);
165 assert!(
166 out.is_empty(),
167 "multi-statement should not match: {:?}",
168 out
169 );
170 }
171}