Skip to main content

codelens_engine/
redundant_definitions.rs

1//! Detects "thin wrapper" definitions — functions whose entire body is a
2//! single call to another function with one literal default argument
3//! (e.g. `pub fn record_X(&self) { self.record_X_for_session(None) }`).
4//!
5//! This is a syntactic-only detector that catches the exact pattern flagged
6//! by self-dogfooding in v1.13.0 Phase 1-A: 16 `record_*` wrappers all
7//! forwarding to their `_for_session(None)` substrate. The substrate could
8//! be flagged by `find_dead_code_v2` only AFTER the wrappers were removed,
9//! so this complement helps surface the cluster pre-deletion.
10
11use crate::project::{collect_files, ProjectRoot};
12use anyhow::Result;
13use regex::Regex;
14use serde::Serialize;
15use std::path::Path;
16use std::sync::LazyLock;
17
18/// Matches a Rust one-line wrapper:
19///   `pub fn NAME(args) [-> RetType] { self.OTHER(args, LITERAL) [;] }`
20///   `fn NAME(args) [-> RetType] { OTHER(args, LITERAL) [;] }`
21/// where LITERAL is one of: `None`, `Default::default()`, `false`, `true`,
22/// a bare integer literal, or a quoted string literal.
23static RUST_ONE_LINE_WRAPPER_RE: LazyLock<Regex> = LazyLock::new(|| {
24    // (?m): multiline (^/$ are line anchors)
25    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
40/// Finds Rust one-line wrappers in the project.
41///
42/// Returns a list of (wrapper, target) pairs. Callers can group by `target`
43/// to find substrates with multiple wrappers — the highest-leverage cleanup
44/// opportunity per Phase 1-A's findings.
45pub 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        // wrapper == target → not flagged (would be infinite recursion, not delegation)
152        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}